2017 年 9 月 9 日
September 9, 2017
一段时间以来,我一直在想写一本针对程序员的关于范畴论的书。请注意,不是计算机科学家,而是程序员——工程师而不是科学家。我知道这听起来很疯狂,而且我确实很害怕。我不能否认科学和工程之间存在巨大的鸿沟,因为我在鸿沟的两边都工作过。但我一直有一种强烈的冲动去解释事情。我非常钦佩理查德·费曼,他是简单解释的大师。我知道我不是费曼,但我会尽力而为。我首先发表这篇序言——旨在激励读者学习范畴论——希望引发讨论并征求反馈。
For some time now I’ve been floating the idea of writing a book about category theory that would be targeted at programmers. Mind you, not computer scientists but programmers — engineers rather than scientists. I know this sounds crazy and I am properly scared. I can’t deny that there is a huge gap between science and engineering because I have worked on both sides of the divide. But I’ve always felt a very strong compulsion to explain things. I have tremendous admiration for Richard Feynman who was the master of simple explanations. I know I’m no Feynman, but I will try my best. I’m starting by publishing this preface — which is supposed to motivate the reader to learn category theory — in hopes of starting a discussion and soliciting feedback.
我将尝试用几段文字让你相信这本书是为你而写的,无论你对在“大量的业余时间”中学习数学最抽象的分支之一有什么反对意见,都是完全没有根据的。
I will attempt, in the space of a few paragraphs, to convince you that this book is written for you, and whatever objections you might have to learning one of the most abstract branches of mathematics in your “copious spare time” are totally unfounded.
我的乐观是基于一些观察。首先,范畴论是极其有用的编程思想的宝库。Haskell 程序员长期以来一直在挖掘这一资源,并且这些想法正在慢慢渗透到其他语言中,但这个过程太慢了。我们需要加快速度。
My optimism is based on several observations. First, category theory is a treasure trove of extremely useful programming ideas. Haskell programmers have been tapping this resource for a long time, and the ideas are slowly percolating into other languages, but this process is too slow. We need to speed it up.
其次,数学有很多种,它们吸引不同的受众。您可能对微积分或代数过敏,但这并不意味着您不会喜欢范畴论。我什至认为范畴论是一种特别适合程序员思维的数学。这是因为范畴论——而不是处理细节——处理的是结构。它涉及使程序可组合的结构。
Second, there are many different kinds of math, and they appeal to different audiences. You might be allergic to calculus or algebra, but it doesn’t mean you won’t enjoy category theory. I would go as far as to argue that category theory is the kind of math that is particularly well suited for the minds of programmers. That’s because category theory — rather than dealing with particulars — deals with structure. It deals with the kind of structure that makes programs composable.
组合是范畴论的根源——它是范畴本身定义的一部分。我会强烈主张组合是编程的本质。早在一些伟大的工程师提出子例程的想法之前,我们就一直在编写东西。不久前,结构化编程的原理彻底改变了编程,因为它们使代码块可组合。然后出现了面向对象编程,它就是关于组合对象。函数式编程不仅涉及组合函数和代数数据结构,它还使并发可组合,这对于其他编程范例来说几乎是不可能的。
Composition is at the very root of category theory — it’s part of the definition of the category itself. And I will argue strongly that composition is the essence of programming. We’ve been composing things forever, long before some great engineer came up with the idea of a subroutine. Some time ago the principles of structural programming revolutionized programming because they made blocks of code composable. Then came object oriented programming, which is all about composing objects. Functional programming is not only about composing functions and algebraic data structures — it makes concurrency composable — something that’s virtually impossible with other programming paradigms.
第三,我有一个秘密武器,一把屠刀,我可以用它屠宰数学,使其更容易被程序员接受。当你是一名专业数学家时,你必须非常小心地弄清所有假设,正确地限定每个陈述,并严格构建所有证明。这使得数学论文和书籍对于外人来说极其难以阅读。我是一名训练有素的物理学家,在物理学中,我们利用非正式推理取得了惊人的进步。数学家嘲笑狄拉克δ函数,它是伟大物理学家PAM狄拉克为了解决一些微分方程而现场编造出来的。当他们发现微积分的一个全新分支,称为分布理论,将狄拉克的见解形式化时,他们停止了笑。
Third, I have a secret weapon, a butcher’s knife, with which I will butcher math to make it more palatable to programmers. When you’re a professional mathematician, you have to be very careful to get all your assumptions straight, qualify every statement properly, and construct all your proofs rigorously. This makes mathematical papers and books extremely hard to read for an outsider. I’m a physicist by training, and in physics we made amazing advances using informal reasoning. Mathematicians laughed at the Dirac delta function, which was made up on the spot by the great physicist P. A. M. Dirac to solve some differential equations. They stopped laughing when they discovered a completely new branch of calculus called distribution theory that formalized Dirac’s insights.
当然,当使用挥手论证时,你可能会冒着说出明显错误的东西的风险,所以我会尽力确保本书中的非正式论证背后有坚实的数学理论。我的床头柜上确实有一本破旧的桑德斯·麦克莱恩(Saunders Mac Lane)的《工作数学家范畴论》 。
Of course when using hand-waving arguments you run the risk of saying something blatantly wrong, so I will try to make sure that there is solid mathematical theory behind informal arguments in this book. I do have a worn-out copy of Saunders Mac Lane’s Category Theory for the Working Mathematician on my nightstand.
由于这是程序员的范畴论,我将使用计算机代码来说明所有主要概念。您可能知道函数式语言比更流行的命令式语言更接近数学。它们还提供了更多的抽象能力。因此,一个自然的诱惑是:你必须先学习 Haskell,然后才能获得丰富的范畴论。但这意味着范畴论在函数式编程之外没有任何应用,而事实并非如此。所以我会提供很多C++的例子。诚然,您必须克服一些丑陋的语法,这些模式可能无法从冗长的背景中脱颖而出,并且您可能被迫进行一些复制和粘贴来代替更高的抽象,但这只是 C++ 程序员的职责。
Since this is category theory for programmers I will illustrate all major concepts using computer code. You are probably aware that functional languages are closer to math than the more popular imperative languages. They also offer more abstracting power. So a natural temptation would be to say: You must learn Haskell before the bounty of category theory becomes available to you. But that would imply that category theory has no application outside of functional programming and that’s simply not true. So I will provide a lot of C++ examples. Granted, you’ll have to overcome some ugly syntax, the patterns might not stand out from the background of verbosity, and you might be forced to do some copy and paste in lieu of higher abstraction, but that’s just the lot of a C++ programmer.
但就 Haskell 而言,你并没有摆脱困境。您不必成为 Haskell 程序员,但您需要它作为一种语言来绘制和记录要在 C++ 中实现的想法。这正是我开始使用 Haskell 的方式。我发现它简洁的语法和强大的类型系统对于理解和实现 C++ 模板、数据结构和算法有很大帮助。但由于我不能指望读者已经了解 Haskell,所以我会慢慢介绍它并解释一切。
But you’re not off the hook as far as Haskell is concerned. You don’t have to become a Haskell programmer, but you need it as a language for sketching and documenting ideas to be implemented in C++. That’s exactly how I got started with Haskell. I found its terse syntax and powerful type system a great help in understanding and implementing C++ templates, data structures, and algorithms. But since I can’t expect the readers to already know Haskell, I will introduce it slowly and explain everything as I go.
如果您是一位经验丰富的程序员,您可能会问自己:我已经编写了这么长时间的代码,而没有担心范畴论或函数方法,那么发生了什么变化?当然,您会情不自禁地注意到,有源源不断的新功能特性正在侵入命令式语言。即使是 Java,面向对象编程的堡垒,C++ 中的 lambda 最近也以疯狂的速度发展——每隔几年就有一个新标准——试图赶上不断变化的世界。所有这些活动都是为了颠覆性的变化,或者我们物理学家所说的相变做准备。如果你继续加热水,它最终会开始沸腾。我们现在处于青蛙的位置,必须决定是否应该继续在越来越热的水中游泳,或者开始寻找替代方案。
If you’re an experienced programmer, you might be asking yourself: I’ve been coding for so long without worrying about category theory or functional methods, so what’s changed? Surely you can’t help but notice that there’s been a steady stream of new functional features invading imperative languages. Even Java, the bastion of object-oriented programming, let the lambdas in C++ has recently been evolving at a frantic pace — a new standard every few years — trying to catch up with the changing world. All this activity is in preparation for a disruptive change or, as we physicist call it, a phase transition. If you keep heating water, it will eventually start boiling. We are now in the position of a frog that must decide if it should continue swimming in increasingly hot water, or start looking for some alternatives.
推动这一重大变革的力量之一是多核革命。流行的编程范式,即面向对象的编程,不会给你带来并发和并行领域的任何东西,反而会鼓励危险和有缺陷的设计。数据隐藏是面向对象的基本前提,当与共享和变异相结合时,就会成为数据竞争的秘诀。将互斥体与其保护的数据相结合的想法很好,但不幸的是,锁不能组合,并且锁隐藏使死锁更容易发生并且更难以调试。
One of the forces that are driving the big change is the multicore revolution. The prevailing programming paradigm, object oriented programming, doesn’t buy you anything in the realm of concurrency and parallelism, and instead encourages dangerous and buggy design. Data hiding, the basic premise of object orientation, when combined with sharing and mutation, becomes a recipe for data races. The idea of combining a mutex with the data it protects is nice but, unfortunately, locks don’t compose, and lock hiding makes deadlocks more likely and harder to debug.
但即使在没有并发的情况下,软件系统日益增长的复杂性也在考验命令式范式的可扩展性的极限。简而言之,副作用正在失控。诚然,有副作用的函数通常很方便且易于编写。原则上,它们的效果可以编码在它们的名称和注释中。一个名为 SetPassword 或 WriteFile 的函数显然会改变某些状态并产生副作用,我们已经习惯了处理这种情况。只有当我们开始在其他有副作用的函数之上编写有副作用的函数时,事情才开始变得棘手。这并不是说副作用本质上是不好的,而是它们隐藏在人们的视线之外,这使得它们无法在更大范围内进行管理。副作用不会扩展,而命令式编程就是关于副作用的。
But even in the absence of concurrency, the growing complexity of software systems is testing the limits of scalability of the imperative paradigm. To put it simply, side effects are getting out of hand. Granted, functions that have side effects are often convenient and easy to write. Their effects can in principle be encoded in their names and in the comments. A function called SetPassword or WriteFile is obviously mutating some state and generating side effects, and we are used to dealing with that. It’s only when we start composing functions that have side effects on top of other functions that have side effects, and so on, that things start getting hairy. It’s not that side effects are inherently bad — it’s the fact that they are hidden from view that makes them impossible to manage at larger scales. Side effects don’t scale, and imperative programming is all about side effects.
硬件的变化和软件日益复杂的趋势迫使我们重新思考编程的基础。就像欧洲伟大的哥特式大教堂的建造者一样,我们一直在将我们的工艺磨练到材料和结构的极限。法国博韦有一座未完工的哥特式大教堂,见证了人类与局限性的深刻斗争。它的目的是打破之前所有的高度和重量记录,但它遭遇了一系列的倒塌。铁棍和木支撑等临时措施防止了它解体,但显然很多事情都出了问题。从现代角度来看,如此多的哥特式结构在没有现代材料科学、计算机建模、有限元分析以及普通数学和物理学的帮助下得以成功完成,真是一个奇迹。我希望子孙后代能够欣赏我们在构建复杂操作系统、网络服务器和互联网基础设施方面所展示的编程技能。而且,坦率地说,他们应该这样做,因为我们所做的这一切都是基于非常脆弱的理论基础。如果我们想前进,就必须夯实这些基础。
Changes in hardware and the growing complexity of software are forcing us to rethink the foundations of programming. Just like the builders of Europe’s great gothic cathedrals we’ve been honing our craft to the limits of material and structure. There is an unfinished gothic cathedral in Beauvais, France, that stands witness to this deeply human struggle with limitations. It was intended to beat all previous records of height and lightness, but it suffered a series of collapses. Ad hoc measures like iron rods and wooden supports keep it from disintegrating, but obviously a lot of things went wrong. From a modern perspective, it’s a miracle that so many gothic structures had been successfully completed without the help of modern material science, computer modelling, finite element analysis, and general math and physics. I hope future generations will be as admiring of the programming skills we’ve been displaying in building complex operating systems, web servers, and the internet infrastructure. And, frankly, they should, because we’ve done all this based on very flimsy theoretical foundations. We have to fix those foundations if we want to move forward.
防止博韦大教堂倒塌的临时措施。
Ad hoc measures preventing the Beauvais cathedral from collapsing.
范畴是一个极其简单的概念。范畴由对象和对象之间的箭头组成。这就是为什么范畴很容易用图形表示的原因。一个物体可以画成一个圆或一个点,而箭头……就是箭头。(为了多样性,我偶尔会把物体画成小猪,把箭画成烟花。)但范畴的本质是构图。或者,如果你愿意的话,组合的本质是一个范畴。箭头是组合的,因此,如果有一个从对象 A 到对象 B 的箭头,以及另一个从对象 B 到对象 C 的箭头,那么必须有一个从 A 到 C 的箭头(它们的组合)。
A category is an embarrassingly simple concept. A category consists of objects and arrows that go between them. That’s why categories are so easy to represent pictorially. An object can be drawn as a circle or a point, and an arrow… is an arrow. (Just for variety, I will occasionally draw objects as piggies and arrows as fireworks.) But the essence of a category is composition. Or, if you prefer, the essence of composition is a category. Arrows compose, so if you have an arrow from object A to object B, and another arrow from object B to object C, then there must be an arrow — their composition — that goes from A to C.
在一个范畴中,如果有一个从A到B的箭头和一个从B到C的箭头,那么也必须有一个从A到C的直接箭头,这就是它们的组合。该图不是一个完整的范畴,因为它缺少恒等态射(见下文)。
In a category, if there is an arrow going from A to B and an arrow going from B to C then there must also be a direct arrow from A to C that is their composition. This diagram is not a full category because it’s missing identity morphisms (see later).
这已经是太多抽象的废话了吗?不要灰心。我们来谈谈具体的吧。将箭头(也称为态射)视为函数。您有一个函数 f,它接受 A 类型的参数并返回 B。您还有另一个函数 g,它接受 B 并返回 C。您可以通过将 f 的结果传递给 g 来组合它们。您刚刚定义了一个接受 A 并返回 C 的新函数。
Is this already too much abstract nonsense? Do not despair. Let’s talk concretes. Think of arrows, which are also called morphisms, as functions. You have a function f that takes an argument of type A and returns a B. You have another function g that takes a B and returns a C. You can compose them by passing the result of f to g. You have just defined a new function that takes an A and returns a C.
在数学中,这种组合由函数之间的小圆圈表示:g∘f。注意从右到左的构图顺序。对于某些人来说,这令人困惑。您可能熟悉 Unix 中的管道表示法,如下所示:
In math, such composition is denoted by a small circle between functions: g∘f. Notice the right to left order of composition. For some people this is confusing. You may be familiar with the pipe notation in Unix, as in:
lsof | grep Chromelsof | grep Chrome
或 F# 中的 V 形符号>>,两者都是从左到右。但在数学和 Haskell 中,函数是从右到左组合的。如果您将 g∘f 读作“ f之后的g ”,将会有所帮助。
or the chevron >> in F#, which both go from left to right. But in mathematics and in Haskell functions compose right to left. It helps if you read g∘f as “g after f.”
让我们通过编写一些 C 代码来使这一点更加明确。我们有一个函数f,它接受一个类型的参数A并返回一个类型的值B:
Let’s make this even more explicit by writing some C code. We have one function f that takes an argument of type A and returns a value of type B:
B f(A a);B f(A a);
另一个:
and another:
C g(B b);C g(B b);
它们的组成是:
Their composition is:
C g_after_f(A a)
{
return g(f(a));
}C g_after_f(A a)
{
return g(f(a));
}
在这里,您再次看到从右到左的组合:g(f(a)); 这次在C。
Here, again, you see right-to-left composition: g(f(a)); this time in C.
我希望我能告诉您,C++ 标准库中有一个模板,它接受两个函数并返回它们的组合,但是没有一个模板。所以让我们尝试一些 Haskell 来改变一下。这是从 A 到 B 的函数声明:
I wish I could tell you that there is a template in the C++ Standard Library that takes two functions and returns their composition, but there isn’t one. So let’s try some Haskell for a change. Here’s the declaration of a function from A to B:
f :: A -> Bf :: A -> B
相似地:
Similarly:
g :: B -> Cg :: B -> C
它们的组成是:
Their composition is:
g . fg . f
一旦你看到 Haskell 中的事情有多么简单,你就会发现无法用 C++ 表达简单的函数概念会有点尴尬。事实上,Haskell 允许你使用 Unicode 字符,这样你就可以将组合写成:
Once you see how simple things are in Haskell, the inability to express straightforward functional concepts in C++ is a little embarrassing. In fact, Haskell will let you use Unicode characters so you can write composition as:
g ∘ fg ∘ f
您甚至可以使用 Unicode 双冒号和箭头:
You can even use Unicode double colons and arrows:
f ∷ A → Bf ∷ A → B
这是 Haskell 的第一课:双冒号表示“具有……的类型” 函数类型是通过在两个类型之间插入箭头来创建的。通过在两个函数之间插入句点(或 Unicode 圆圈)来组成两个函数。
So here’s the first Haskell lesson: Double colon means “has the type of…” A function type is created by inserting an arrow between two types. You compose two functions by inserting a period between them (or a Unicode circle).
任何范畴的组合物都必须满足两个极其重要的属性。
There are two extremely important properties that the composition in any category must satisfy.
1. 组合具有结合性。如果您有三个可组合的态射 f、g 和 h(即它们的对象端到端匹配),则不需要括号来组合它们。用数学符号表示为:
1. Composition is associative. If you have three morphisms, f, g, and h, that can be composed (that is, their objects match end-to-end), you don’t need parentheses to compose them. In math notation this is expressed as:
h∘(g∘f) = (h∘g)∘f = h∘g∘fh∘(g∘f) = (h∘g)∘f = h∘g∘f
在(伪)Haskell 中:
In (pseudo) Haskell:
f :: A -> B
g :: B -> C
h :: C -> D
h . (g . f) == (h . g) . f == h . g . ff :: A -> B
g :: B -> C
h :: C -> D
h . (g . f) == (h . g) . f == h . g . f
(我说“伪”,因为没有为函数定义相等性。)
(I said “pseudo,” because equality is not defined for functions.)
结合性在处理函数时非常明显,但在其他范畴中可能不那么明显。
Associativity is pretty obvious when dealing with functions, but it may be not as obvious in other categories.
2. 对于每个对象 A 都有一个箭头,它是一个组合单元。该箭头从对象循环到自身。作为组合单位意味着,当与分别从 A 开始或以 A 结束的任何箭头组合时,它会返回相同的箭头。对象 A 的单位箭头称为 id A(A 上的身份)。用数学表示法来说,如果 f 从 A 到 B 那么
2. For every object A there is an arrow which is a unit of composition. This arrow loops from the object to itself. Being a unit of composition means that, when composed with any arrow that either starts at A or ends at A, respectively, it gives back the same arrow. The unit arrow for object A is called idA (identity on A). In math notation, if f goes from A to B then
f∘idA = ff∘idA = f
和
and
idB∘f = fidB∘f = f
在处理函数时,恒等箭头被实现为仅返回其参数的恒等函数。每种类型的实现都是相同的,这意味着该函数是普遍多态的。在 C++ 中我们可以将其定义为模板:
When dealing with functions, the identity arrow is implemented as the identity function that just returns back its argument. The implementation is the same for every type, which means this function is universally polymorphic. In C++ we could define it as a template:
template T id(T x) { return x; }template T id(T x) { return x; }
当然,在 C++ 中没有那么简单,因为您不仅要考虑传递的内容,还要考虑传递的方式(即按值、按引用、按常量引用、按移动等)。
Of course, in C++ nothing is that simple, because you have to take into account not only what you’re passing but also how (that is, by value, by reference, by const reference, by move, and so on).
在 Haskell 中,恒等函数是标准库的一部分(称为 Prelude)。这是它的声明和定义:
In Haskell, the identity function is part of the standard library (called Prelude). Here’s its declaration and definition:
id :: a -> a
id x = xid :: a -> a
id x = x
正如你所看到的,Haskell 中的多态函数是小菜一碟。在声明中,您只需将类型替换为类型变量即可。技巧如下:具体类型的名称始终以大写字母开头,类型变量的名称以小写字母开头。所以这里a代表所有类型。
As you can see, polymorphic functions in Haskell are a piece of cake. In the declaration, you just replace the type with a type variable. Here’s the trick: names of concrete types always start with a capital letter, names of type variables start with a lowercase letter. So here a stands for all types.
Haskell 函数定义由函数名称和形式参数组成——这里只有一个x. 函数体跟在等号后面。这种简洁常常会让新手感到震惊,但你很快就会发现它很有道理。函数定义和函数调用是函数式编程的基础,因此它们的语法被减少到最低限度。不仅参数列表周围没有括号,而且参数之间也没有逗号(稍后,当我们定义多个参数的函数时,您会看到这一点)。
Haskell function definitions consist of the name of the function followed by formal parameters — here just one, x. The body of the function follows the equal sign. This terseness is often shocking to newcomers but you will quickly see that it makes perfect sense. Function definition and function call are the bread and butter of functional programming so their syntax is reduced to the bare minimum. Not only are there no parentheses around the argument list but there are no commas between arguments (you’ll see that later, when we define functions of multiple arguments).
函数体始终是一个表达式——函数中没有语句。函数的结果就是这个表达式——这里只是x。
The body of a function is always an expression — there are no statements in functions. The result of a function is this expression — here, just x.
我们的第二节 Haskell 课程到此结束。
This concludes our second Haskell lesson.
恒等条件可以写成(同样,用伪 Haskell):
The identity conditions can be written (again, in pseudo-Haskell) as:
f . id == f
id . f == ff . id == f
id . f == f
您可能会问自己这样一个问题:为什么有人会为恒等函数(一个什么都不做的函数)烦恼?话又说回来,我们为什么要为数字零烦恼呢?零是无意义的象征。古罗马人有一种没有零的数字系统,他们能够建造出色的道路和渡槽,其中一些至今仍存在。
You might be asking yourself the question: Why would anyone bother with the identity function — a function that does nothing? Then again, why do we bother with the number zero? Zero is a symbol for nothing. Ancient Romans had a number system without a zero and they were able to build excellent roads and aqueducts, some of which survive to this day.
中性值(例如零)或id在处理符号变量时非常有用。这就是为什么罗马人不太擅长代数,而熟悉零概念的阿拉伯人和波斯人却很擅长。因此,恒等函数作为高阶函数的参数或返回变得非常方便。高阶函数使得函数的符号操作成为可能。它们是函数的代数。
Neutral values like zero or id are extremely useful when working with symbolic variables. That’s why Romans were not very good at algebra, whereas the Arabs and the Persians, who were familiar with the concept of zero, were. So the identity function becomes very handy as an argument to, or a return from, a higher-order function. Higher order functions are what make symbolic manipulation of functions possible. They are the algebra of functions.
总结一下:范畴由对象和箭头(态射)组成。箭头可以组合,并且组合是关联的。每个对象都有一个恒等箭头,充当组合下的一个单元。
To summarize: A category consists of objects and arrows (morphisms). Arrows can be composed, and the composition is associative. Every object has an identity arrow that serves as a unit under composition.
函数式程序员有一种独特的处理问题的方式。他们首先提出非常禅宗般的问题。比如设计一个交互程序时,他们会问:什么是交互?在实施康威的生命游戏时,他们可能会思考生命的意义。本着这种精神,我要问:什么是编程?在最基本的层面上,编程就是告诉计算机要做什么。“取出内存地址x的内容并将其添加到寄存器EAX的内容中。” 但即使我们用汇编语言编程,我们给计算机的指令也是更有意义的表达。我们正在解决一个不平凡的问题(如果它是微不足道的,我们就不需要计算机的帮助)。以及我们如何解决问题?我们将更大的问题分解为更小的问题。如果较小的问题仍然太大,我们会进一步分解它们,依此类推。最后,我们编写解决所有小问题的代码。然后是编程的本质:我们编写这些代码片段来创建更大问题的解决方案。如果我们无法将碎片重新组合在一起,那么分解就没有意义。
Functional programmers have a peculiar way of approaching problems. They start by asking very Zen-like questions. For instance, when designing an interactive program, they would ask: What is interaction? When implementing Conway’s Game of Life, they would probably ponder about the meaning of life. In this spirit, I’m going to ask: What is programming? At the most basic level, programming is about telling the computer what to do. “Take the contents of memory address x and add it to the contents of the register EAX.” But even when we program in assembly, the instructions we give the computer are an expression of something more meaningful. We are solving a non-trivial problem (if it were trivial, we wouldn’t need the help of the computer). And how do we solve problems? We decompose bigger problems into smaller problems. If the smaller problems are still too big, we decompose them further, and so on. Finally, we write code that solves all the small problems. And then comes the essence of programming: we compose those pieces of code to create solutions to larger problems. Decomposition wouldn’t make sense if we weren’t able to put the pieces back together.
这种层次分解和重组的过程并不是计算机强加给我们的。它反映了人类思维的局限性。我们的大脑一次只能处理少量的概念。心理学中被引用最多的论文之一《神奇的数字七,加或减二》假设我们只能在脑海中保留 7±2 个“块”信息。我们对人类短期记忆的理解细节可能正在发生变化,但我们确信它是有限的。最重要的是,我们无法处理大量的对象或意大利面条般的代码。我们需要结构,并不是因为结构良好的程序看起来赏心悦目,而是因为否则我们的大脑就无法有效地处理它们。我们经常将某些代码描述为优雅或美丽,但我们真正的意思是它很容易被我们有限的人类思维处理。优雅的代码会创建大小合适、数量合适的块,以便我们的心理消化系统吸收它们。
This process of hierarchical decomposition and recomposition is not imposed on us by computers. It reflects the limitations of the human mind. Our brains can only deal with a small number of concepts at a time. One of the most cited papers in psychology, The Magical Number Seven, Plus or Minus Two, postulated that we can only keep 7 ± 2 “chunks” of information in our minds. The details of our understanding of the human short-term memory might be changing, but we know for sure that it’s limited. The bottom line is that we are unable to deal with the soup of objects or the spaghetti of code. We need structure not because well-structured programs are pleasant to look at, but because otherwise our brains can’t process them efficiently. We often describe some piece of code as elegant or beautiful, but what we really mean is that it’s easy to process by our limited human minds. Elegant code creates chunks that are just the right size and come in just the right number for our mental digestive system to assimilate them.
那么组成程序的正确块是什么?它们的表面积的增加速度必须慢于其体积的增加速度。(我喜欢这个类比,因为我的直觉是,几何物体的表面积随其尺寸的平方而增长——比体积随其尺寸的立方而增长的速度慢。)表面积是我们需要的信息组成块。体积是我们实现它们所需的信息。这个想法是,一旦实现了一个块,我们就可以忘记其实现的细节,而专注于它如何与其他块交互。在面向对象编程中,表面是对象的类声明,或其抽象接口。在函数式编程中,它是函数的声明。(我稍微简化了一些事情,但这就是要点。)
So what are the right chunks for the composition of programs? Their surface area has to increase slower than their volume. (I like this analogy because of the intuition that the surface area of a geometric object grows with the square of its size — slower than the volume, which grows with the cube of its size.) The surface area is the information we need in order to compose chunks. The volume is the information we need in order to implement them. The idea is that, once a chunk is implemented, we can forget about the details of its implementation and concentrate on how it interacts with other chunks. In object-oriented programming, the surface is the class declaration of the object, or its abstract interface. In functional programming, it’s the declaration of a function. (I’m simplifying things a bit, but that’s the gist of it.)
范畴论是极端的,因为它积极阻止我们观察对象的内部。范畴论中的对象是一个抽象的模糊实体。你所能知道的就是它如何与其他对象相关——它如何使用箭头与它们连接。这就是互联网搜索引擎通过分析传入和传出链接对网站进行排名的方式(除非它们作弊)。在面向对象编程中,理想化的对象只能通过其抽象接口(纯表面,无体积)可见,方法扮演箭头的角色。当您必须深入研究对象的实现以了解如何将其与其他对象组合时,您就失去了编程范例的优势。
Category theory is extreme in the sense that it actively discourages us from looking inside the objects. An object in category theory is an abstract nebulous entity. All you can ever know about it is how it relates to other object — how it connects with them using arrows. This is how internet search engines rank web sites by analyzing incoming and outgoing links (except when they cheat). In object-oriented programming, an idealized object is only visible through its abstract interface (pure surface, no volume), with methods playing the role of arrows. The moment you have to dig into the implementation of the object in order to understand how to compose it with other objects, you’ve lost the advantages of your programming paradigm.
关于静态与动态以及强类型与弱类型的优点似乎存在一些争议。让我用一个思想实验来说明这些选择。想象一下,数以百万计的猴子在电脑键盘上愉快地敲击随机按键,生成程序、编译和运行它们。
There seems to be some controversy about the advantages of static vs. dynamic and strong vs. weak typing. Let me illustrate these choices with a thought experiment. Imagine millions of monkeys at computer keyboards happily hitting random keys, producing programs, compiling, and running them.
使用机器语言,猴子产生的任何字节组合都将被接受并运行。但对于高级语言,我们确实很欣赏编译器能够检测词汇和语法错误的事实。很多猴子会没有香蕉,但剩下的程序将有更好的机会发挥作用。类型检查为无意义的程序提供了另一个障碍。此外,在动态类型语言中,类型不匹配会在运行时被发现,而在强类型静态检查语言中,类型不匹配是在编译时发现的,从而在有机会运行之前消除大量不正确的程序。
With machine language, any combination of bytes produced by monkeys would be accepted and run. But with higher level languages, we do appreciate the fact that a compiler is able to detect lexical and grammatical errors. Lots of monkeys will go without bananas, but the remaining programs will have a better chance of being useful. Type checking provides yet another barrier against nonsensical programs. Moreover, whereas in a dynamically typed language, type mismatches would be discovered at runtime, in strongly typed statically checked languages type mismatches are discovered at compile time, eliminating lots of incorrect programs before they have a chance to run.
那么问题来了,我们是想让猴子开心,还是想制作出正确的程序?
So the question is, do we want to make monkeys happy, or do we want to produce correct programs?
打字猴思维实验的通常目标是制作莎士比亚全集。在循环中使用拼写检查器和语法检查器将大大增加可能性。类型检查器的类比可以更进一步,确保一旦罗密欧被宣布为人类,他不会在其强大的引力场中发芽或捕获光子。
The usual goal in the typing monkeys thought experiment is the production of the complete works of Shakespeare. Having a spell checker and a grammar checker in the loop would drastically increase the odds. The analog of a type checker would go even further by making sure that, once Romeo is declared a human being, he doesn’t sprout leaves or trap photons in his powerful gravitational field.
范畴论是关于组合箭头的。但任意两个箭头都不能组合。一个箭头的目标对象必须与下一个箭头的源源对象相同。在编程中,我们将一个函数的结果传递给另一个函数。如果目标函数无法正确解释源函数生成的数据,则程序将无法运行。两端必须适合组合物才能发挥作用。语言的类型系统越强大,这种匹配的描述和机械验证就越好。
Category theory is about composing arrows. But not any two arrows can be composed. The target object of one arrow must be the same as the source source object of the next arrow. In programming we pass the results on one function to another. The program will not work if the target function is not able to correctly interpret the data produced by the source function. The two ends must fit for the composition to work. The stronger the type system of the language, the better this match can be described and mechanically verified.
我听到的反对强静态类型检查的唯一严肃的论点是,它可能会消除一些语义上正确的程序。实际上,这种情况很少发生,并且无论如何,每种语言都会在确实有必要时提供某种后门来绕过类型系统。甚至哈斯克尔也有unsafeCoerce。但此类设备应谨慎使用。弗兰兹·卡夫卡的角色格雷戈尔·萨姆萨在变身为一只巨大的虫子时打破了类型系统,我们都知道它的结局。
The only serious argument I hear against strong static type checking is that it might eliminate some programs that are semantically correct. In practice, this happens extremely rarely and, in any case, every language provides some kind of a backdoor to bypass the type system when that’s really necessary. Even Haskell has unsafeCoerce. But such devices should by used judiciously. Franz Kafka’s character, Gregor Samsa, breaks the type system when he metamorphoses into a giant bug, and we all know how it ends.
我经常听到的另一个论点是,处理类型给程序员带来了太多负担。在我自己用 C++ 编写了一些迭代器声明之后,我可能会同情这种情绪,除了有一种称为类型推断的技术,它可以让编译器从使用它们的上下文中推断出大多数类型。在 C++ 中,您现在可以声明变量auto并让编译器确定其类型。
Another argument I hear a lot is that dealing with types imposes too much burden on the programmer. I could sympathize with this sentiment after having to write a few declarations of iterators in C++ myself, except that there is a technology called type inference that lets the compiler deduce most of the types from the context in which they are used. In C++, you can now declare a variable auto and let the compiler figure out its type.
在 Haskell 中,除了极少数情况外,类型注释完全是可选的。程序员无论如何都倾向于使用它们,因为它们可以告诉我们很多关于代码的语义,并且它们使编译错误更容易理解。通过设计类型来启动项目是 Haskell 中的常见做法。后来,类型注释驱动实现并成为编译器强制注释。
In Haskell, except on rare occasions, type annotations are purely optional. Programmers tend to use them anyway, because they can tell a lot about the semantics of code, and they make compilation errors easier to understand. It’s a common practice in Haskell to start a project by designing the types. Later, type annotations drive the implementation and become compiler-enforced comments.
强静态类型经常被用作不测试代码的借口。有时你可能会听到 Haskell 程序员说:“如果它能编译,它一定是正确的。” 当然,不能保证类型正确的程序在产生正确输出的意义上是正确的。这种漫不经心的态度的结果是,在多项研究中,Haskell 在代码质量方面并没有像人们预期的那样遥遥领先。看来,在商业环境中,修复错误的压力只施加到一定的质量水平,这与软件开发的经济性和最终用户的容忍度密切相关,而与软件开发的成本无关。编程语言或方法。更好的标准是衡量有多少项目落后于计划或交付的功能大幅减少。
Strong static typing is often used as an excuse for not testing the code. You may sometimes hear Haskell programmers saying, “If it compiles, it must be correct.” Of course, there is no guarantee that a type-correct program is correct in the sense of producing the right ouput. The result of this cavalier attitude is that in several studies Haskell didn’t come as strongly ahead of the pack in code quality as one would expect. It seems that, in the commercial setting, the pressure to fix bugs is applied only up to a certain quality level, which has everything to do with the economics of software development and the tolerance of the end user, and very little to do with the programming language or methodology. A better criterion would be to measure how many projects fall behind schedule or are delivered with drastically reduced functionality.
至于单元测试可以取代强类型的论点,请考虑强类型语言中常见的重构实践:更改特定函数的参数类型。在强类型语言中,修改该函数的声明然后修复所有构建中断就足够了。在弱类型语言中,函数现在需要不同数据的事实无法传播到调用站点。单元测试可能会发现一些不匹配的情况,但测试几乎总是一个概率性过程,而不是一个确定性过程。测试并不能很好地替代证据。
As for the argument that unit testing can replace strong typing, consider the common refactoring practice in strongly typed languages: changing the type of an argument of a particular function. In a strongly typed language, it’s enough to modify the declaration of that function and then fix all the build breaks. In a weakly typed language, the fact that a function now expects different data cannot be propagated to call sites. Unit testing may catch some of the mismatches, but testing is almost always a probabilistic rather than a deterministic process. Testing is a poor substitute for proof.
对于类型最简单的直觉是它们是值的集合。类型(请记住,具体类型在 Haskell 中以大写字母开头)是和Bool的二元素集。类型是所有 Unicode 字符的集合,例如或。TrueFalseChar'a''ą'
The simplest intuition for types is that they are sets of values. The type Bool (remember, concrete types start with a capital letter in Haskell) is a two-element set of True and False. Type Char is a set of all Unicode characters like 'a' or 'ą'.
集合可以是有限的或无限的。的类型String是 的列表的同义词Char,是无限集的一个示例。
Sets can be finite or infinite. The type of String, which is a synonym for a list of Char, is an example of an infinite set.
当我们声明x为Integer:
When we declare x to be an Integer:
x :: Integerx :: Integer
我们说它是整数集合的一个元素。Integer在Haskell中是一个无限集,它可以用来做任意精度的算术。还有一个Int与机器类型对应的有限集,就像 C++ 一样int。
we are saying that it’s an element of the set of integers. Integer in Haskell is an infinite set, and it can be used to do arbitrary precision arithmetic. There is also a finite-set Int that corresponds to machine type, just like the C++ int.
有一些微妙之处使得类型和集合的识别变得棘手。涉及循环定义的多态函数存在问题,并且不能拥有所有集合的集合;但正如我所承诺的,我不会坚持数学。最棒的是有一类集合,称为Set,我们将直接使用它。在Set中,对象是集合,态射(箭头)是函数。
There are some subtleties that make this identification of types and sets tricky. There are problems with polymorphic functions that involve circular definitions, and with the fact that you can’t have a set of all sets; but as I promised, I won’t be a stickler for math. The great thing is that there is a category of sets, which is called Set, and we’ll just work with it. In Set, objects are sets and morphisms (arrows) are functions.
集合是一个非常特殊的范畴,因为我们实际上可以窥视它的对象内部并从中获得很多直觉。例如,我们知道空集没有元素。我们知道存在特殊的单元素集。我们知道函数将一个集合的元素映射到另一集合的元素。它们可以将两个元素映射到一个元素,但不能将一个元素映射到两个元素。我们知道恒等函数将集合中的每个元素映射到其自身,依此类推。计划是逐渐忘记所有这些信息,并用纯粹的分类术语(即对象和箭头)表达所有这些概念。
Set is a very special category, because we can actually peek inside its objects and get a lot of intuitions from doing that. For instance, we know that an empty set has no elements. We know that there are special one-element sets. We know that functions map elements of one set to elements of another set. They can map two elements to one, but not one element to two. We know that an identity function maps each element of a set to itself, and so on. The plan is to gradually forget all this information and instead express all those notions in purely categorical terms, that is in terms of objects and arrows.
在理想的世界中,我们只会说 Haskell 类型是集合,Haskell 函数是集合之间的数学函数。只有一个小问题:数学函数不执行任何代码——它只知道答案。Haskell 函数必须计算答案。如果可以在有限数量的步骤中获得答案,那么这不是问题——无论这个数字有多大。但有些计算涉及递归,并且这些计算可能永远不会终止。我们不能仅仅禁止 Haskell 中的非终止函数,因为终止函数和非终止函数之间的区别是不可判定的——这就是著名的停机问题。这就是为什么计算机科学家想出了一个绝妙的主意,或者一个重大的黑客,根据你的观点,将每种类型扩展一个更特殊的值,称为底部,并用 或 Unicode ⊥表示_|_。这个“值”对应于一个非终止的计算。所以函数声明为:
In the ideal world we would just say that Haskell types are sets and Haskell functions are mathematical functions between sets. There is just one little problem: A mathematical function does not execute any code — it just knows the answer. A Haskell function has to calculate the answer. It’s not a problem if the answer can be obtained in a finite number of steps — however big that number might be. But there are some calculations that involve recursion, and those might never terminate. We can’t just ban non-terminating functions from Haskell because distinguishing between terminating and non-terminating functions is undecidable — the famous halting problem. That’s why computer scientists came up with a brilliant idea, or a major hack, depending on your point of view, to extend every type by one more special value called the bottom and denoted by _|_, or Unicode ⊥. This “value” corresponds to a non-terminating computation. So a function declared as:
f :: Bool -> Boolf :: Bool -> Bool
可能会返回True, False, 或_|_; 后者意味着它永远不会终止。
may return True, False, or _|_; the latter meaning that it would never terminate.
有趣的是,一旦接受底部作为类型系统的一部分,就可以方便地将每个运行时错误视为底部,甚至允许函数显式返回底部。后者通常使用表达式 完成undefined,如下所示:
Interestingly, once you accept the bottom as part of the type system, it is convenient to treat every runtime error as a bottom, and even allow functions to return the bottom explicitly. The latter is usually done using the expression undefined, as in:
f :: Bool -> Bool
f x = undefinedf :: Bool -> Bool
f x = undefined
此定义类型检查,因为undefined计算结果为 Bottom,它是任何类型的成员,包括Bool. 你甚至可以写:
This definition type checks because undefined evaluates to bottom, which is a member of any type, including Bool. You can even write:
f :: Bool -> Bool
f = undefinedf :: Bool -> Bool
f = undefined
(不带x)因为底部也是成员类型Bool->Bool。
(without the x) because the bottom is also a member of the type Bool->Bool.
可能返回底部的函数称为部分函数,而不是全部函数,后者为每个可能的参数返回有效结果。
Functions that may return bottom are called partial, as opposed to total functions, which return valid results for every possible argument.
由于底部,您将看到 Haskell 类型和函数的范畴被称为Hask而不是Set。从理论上来看,这就是永无休止的复杂性的根源,所以此时我将使用我的屠刀,终止这条推理路线。从实用的角度来看,忽略非终止函数和底部是可以的,并将Hask视为真正的Set(请参阅最后的参考书目)。
Because of the bottom, you’ll see the category of Haskell types and functions referred to as Hask rather than Set. From the theoretical point of view, this is the source of never-ending complications, so at this point I will use my butcher’s knife and terminate this line of reasoning. From the pragmatic point of view, it’s okay to ignore non-terminating functions and bottoms, and treat Hask as bona fide Set (see Bibliography at the end).
作为一名程序员,您非常熟悉编程语言的语法和语法。语言的这些方面通常在语言规范的开头使用正式符号进行描述。但语言的含义或语义更难描述;它需要更多的页数,很少不够正式,而且几乎永远不会完整。因此,语言律师之间永无休止的讨论,以及致力于解释语言标准细节的书籍的整个家庭手工业。
As a programmer you are intimately familiar with the syntax and grammar of your programming language. These aspects of the language are usually described using formal notation at the very beginning of the language spec. But the meaning, or semantics, of the language is much harder to describe; it takes many more pages, is rarely formal enough, and almost never complete. Hence the never ending discussions among language lawyers, and a whole cottage industry of books dedicated to the exegesis of the finer points of language standards.
有一些正式的工具可以用来描述语言的语义,但由于其复杂性,它们大多用于简化的学术语言,而不是现实生活中的编程庞然大物。一种称为操作语义的工具描述了程序执行的机制。它定义了一个形式化的理想化解释器。工业语言(例如 C++)的语义通常使用非正式的操作推理来描述,通常用“抽象机器”来描述。
There are formal tools for describing the semantics of a language but, because of their complexity, they are mostly used with simplified academic languages, not real-life programming behemoths. One such tool called operational semantics describes the mechanics of program execution. It defines a formalized idealized interpreter. The semantics of industrial languages, such as C++, is usually described using informal operational reasoning, often in terms of an “abstract machine.”
问题在于,很难使用操作语义来证明程序的相关内容。要显示程序的属性,您本质上必须通过理想化的解释器“运行它”。
The problem is that it’s very hard to prove things about programs using operational semantics. To show a property of a program you essentially have to “run it” through the idealized interpreter.
程序员从不执行正式的正确性证明并不重要。我们总是“认为”我们编写了正确的程序。没有人坐在键盘前说:“哦,我只需输入几行代码,看看会发生什么。” 我们认为我们编写的代码将执行某些操作,从而产生所需的结果。当事实并非如此时,我们通常会感到非常惊讶。这意味着我们对编写的程序进行推理,并且通常是通过在头脑中运行解释器来实现的。跟踪所有变量确实很难。计算机擅长运行程序,而人类则不然!如果是的话,我们就不需要计算机了。
It doesn’t matter that programmers never perform formal proofs of correctness. We always “think” that we write correct programs. Nobody sits at the keyboard saying, “Oh, I’ll just throw a few lines of code and see what happens.” We think that the code we write will perform certain actions that will produce desired results. We are usually quite surprised when it doesn’t. That means we do reason about programs we write, and we usually do it by running an interpreter in our heads. It’s just really hard to keep track of all the variables. Computers are good at running programs — humans are not! If we were, we wouldn’t need computers.
但还有一个替代方案。它称为指称语义,它基于数学。在指称语义中,每个编程结构都被赋予了其数学解释。这样,如果你想证明一个程序的属性,你只需证明一个数学定理即可。你可能认为定理证明很难,但事实是,我们人类几千年来一直在建立数学方法,因此有大量积累的知识可供利用。此外,与专业数学家证明的定理相比,我们在编程中遇到的问题通常非常简单,甚至微不足道。
But there is an alternative. It’s called denotational semantics and it’s based on math. In denotational semantics every programing construct is given its mathematical interpretation. With that, if you want to prove a property of a program, you just prove a mathematical theorem. You might think that theorem proving is hard, but the fact is that we humans have been building up mathematical methods for thousands of years, so there is a wealth of accumulated knowledge to tap into. Also, as compared to the kind of theorems that professional mathematicians prove, the problems that we encounter in programming are usually quite simple, if not trivial.
考虑 Haskell 中阶乘函数的定义,这是一种非常适合指称语义的语言:
Consider the definition of a factorial function in Haskell, which is a language quite amenable to denotational semantics:
fact n = product [1..n]fact n = product [1..n]
表达式[1..n]是从 1 到 n 的整数列表。该函数product将列表中的所有元素相乘。这就像数学课本中阶乘的定义一样。与 C 比较:
The expression [1..n] is a list of integers from 1 to n. The function product multiplies all elements of a list. That’s just like a definition of factorial taken from a math text. Compare this with C:
int fact(int n)
{
int i;
int result = 1;
for (i = 2; i <= n; ++i)
result *= i;
return result;
}int fact(int n)
{
int i;
int result = 1;
for (i = 2; i <= n; ++i)
result *= i;
return result;
}
需要我多说?
Need I say more?
好吧,我第一个承认这是一个廉价的镜头!阶乘函数具有明显的数学含义。精明的读者可能会问:从键盘读取字符或通过网络发送数据包的数学模型是什么?在很长一段时间里,这都是一个尴尬的问题,导致了相当复杂的解释。似乎指称语义并不最适合大量重要任务,这些任务对于编写有用的程序至关重要,并且可以通过操作语义轻松解决。突破来自范畴论。Eugenio Moggi 发现计算效果可以映射到单子。事实证明,这是一个重要的观察结果,它不仅给指称语义带来了新的生命,使纯函数式程序更加可用,而且还为传统编程带来了新的曙光。稍后,当我们开发更多分类工具时,我将讨论单子。
Okay, I’ll be the first to admit that this was a cheap shot! A factorial function has an obvious mathematical denotation. An astute reader might ask: What’s the mathematical model for reading a character from the keyboard or sending a packet across the network? For the longest time that would have been an awkward question leading to a rather convoluted explanation. It seemed like denotational semantics wasn’t the best fit for a considerable number of important tasks that were essential for writing useful programs, and which could be easily tackled by operational semantics. The breakthrough came from category theory. Eugenio Moggi discovered that computational effect can be mapped to monads. This turned out to be an important observation that not only gave denotational semantics a new lease on life and made pure functional programs more usable, but also shed new light on traditional programming. I’ll talk about monads later, when we develop more categorical tools.
拥有编程数学模型的重要优点之一是可以对软件的正确性进行正式证明。当您编写消费者软件时,这似乎并不那么重要,但在某些编程领域,失败的代价可能会很高,或者人的生命受到威胁。但即使在为医疗系统编写 Web 应用程序时,您也可能会欣赏 Haskell 标准库中的函数和算法带有正确性证明的想法。
One of the important advantages of having a mathematical model for programming is that it’s possible to perform formal proofs of correctness of software. This might not seem so important when you’re writing consumer software, but there are areas of programming where the price of failure may be exorbitant, or where human life is at stake. But even when writing web applications for the health system, you may appreciate the thought that functions and algorithms from the Haskell standard library come with proofs of correctness.
我们在 C++ 或任何其他命令式语言中称为函数的东西与数学家称为函数的东西不同。数学函数只是值到值的映射。
The things we call functions in C++ or any other imperative language, are not the same things mathematicians call functions. A mathematical function is just a mapping of values to values.
我们可以用编程语言实现一个数学函数:这样的函数,给定输入值将计算输出值。生成数字平方的函数可能会将输入值乘以自身。每次调用它时都会执行此操作,并且每次使用相同的输入调用它时都保证产生相同的输出。数字的平方不随月相变化。
We can implement a mathematical function in a programming language: Such a function, given an input value will calculate the output value. A function to produce a square of a number will probably multiply the input value by itself. It will do it every time it’s called, and it’s guaranteed to produce the same output every time it’s called with the same input. The square of a number doesn’t change with the phases of the Moon.
此外,计算数字的平方不应该对为您的狗分配美味佳肴产生副作用。做到这一点的“函数”不能轻易地建模为数学函数。
Also, calculating the square of a number should not have a side effect of dispensing a tasty treat for your dog. A “function” that does that cannot be easily modelled as a mathematical function.
在编程语言中,在给定相同输入的情况下始终产生相同结果并且没有副作用的函数称为纯函数。在像 Haskell 这样的纯函数式语言中,所有函数都是纯函数。因此,更容易为这些语言赋予指称语义并使用范畴论对其进行建模。至于其他语言,总是可以将自己限制在一个纯子集,或者单独推理副作用。稍后我们将看到 monad 如何让我们仅使用纯函数来建模各种效果。因此,将自己限制在数学函数上确实不会造成任何损失。
In programming languages, functions that always produce the same result given the same input and have no side effects are called pure functions. In a pure functional language like Haskell all functions are pure. Because of that, it’s easier to give these languages denotational semantics and model them using category theory. As for other languages, it’s always possible to restrict yourself to a pure subset, or reason about side effects separately. Later we’ll see how monads let us model all kinds of effects using only pure functions. So we really don’t lose anything by restricting ourselves to mathematical functions.
一旦您意识到类型是集合,您就可以想到一些相当奇特的类型。例如,空集对应的类型是什么?不,它不是 C++ void,尽管这种类型在 Haskell 中被调用Void。它是一种不包含任何值的类型。您可以定义一个接受 的函数Void,但您永远不能调用它。要调用它,您必须提供 type 的值Void,但实际上没有。至于这个函数可以返回什么,没有任何限制。它可以返回任何类型(尽管它永远不会,因为它无法被调用)。换句话说,它是一个返回类型是多态的函数。Haskellers 给它起了个名字:
Once you realize that types are sets, you can think of some rather exotic types. For instance, what’s the type corresponding to an empty set? No, it’s not C++ void, although this type is called Void in Haskell. It’s a type that’s not inhabited by any values. You can define a function that takes Void, but you can never call it. To call it, you would have to provide a value of the type Void, and there just aren’t any. As for what this function can return, there are no restrictions whatsoever. It can return any type (although it never will, because it can’t be called). In other words it’s a function that’s polymorphic in the return type. Haskellers have a name for it:
absurd :: Void -> aabsurd :: Void -> a
(记住,a是一个可以代表任何类型的类型变量。)这个名称并非巧合。从逻辑角度对类型和函数有更深入的解释,称为 Curry-Howard 同构。类型Void代表虚假,而函数的类型absurd对应于任何事物都源自虚假的陈述,如拉丁语格言“ex falso sequitur quodlibet”。
(Remember, a is a type variable that can stand for any type.) The name is not coincidental. There is deeper interpretation of types and functions in terms of logic called the Curry-Howard isomorphism. The type Void represents falsity, and the type of the function absurd corresponds to the statement that from falsity follows anything, as in the Latin adage “ex falso sequitur quodlibet.”
接下来是与单例集对应的类型。它是一种只有一个可能值的类型。这个值只是“是”。您可能不会立即认出它,但这就是 C++ void。考虑这种类型的函数。函数 fromvoid始终可以被调用。如果它是纯函数,它将始终返回相同的结果。这是此类函数的示例:
Next is the type that corresponds to a singleton set. It’s a type that has only one possible value. This value just “is.” You might not immediately recognise it as such, but that is the C++ void. Think of functions from and to this type. A function from void can always be called. If it’s a pure function, it will always return the same result. Here’s an example of such a function:
int f44() { return 44; }int f44() { return 44; }
您可能会认为这个函数不接受“任何东西”,但正如我们刚刚看到的,一个不接受“任何东西”的函数永远不会被调用,因为没有值代表“什么也不做”。那么这个函数需要什么呢?从概念上讲,它需要一个虚拟值,该值只有一个实例,因此我们不必明确提及它。然而,在 Haskell 中,这个值有一个符号:一对空括号,()。因此,出于一个有趣的巧合(或者这是一个巧合?),对 void 函数的调用在 C++ 和 Haskell 中看起来是相同的。另外,由于 Haskell 喜欢简洁,因此()类型、构造函数以及与单例集相对应的唯一值使用相同的符号。这是 Haskell 中的这个函数:
You might think of this function as taking “nothing”, but as we’ve just seen, a function that takes “nothing” can never be called because there is no value representing “nothing.” So what does this function take? Conceptually, it takes a dummy value of which there is only one instance ever, so we don’t have to mention it explicitly. In Haskell, however, there is a symbol for this value: an empty pair of parentheses, (). So, by a funny coincidence (or is it a coincidence?), the call to a function of void looks the same in C++ and in Haskell. Also, because of the Haskell’s love of terseness, the same symbol () is used for the type, the constructor, and the only value corresponding to a singleton set. So here’s this function in Haskell:
f44 :: () -> Integer
f44 () = 44f44 :: () -> Integer
f44 () = 44
第一行声明f44将 type (),发音为“unit”,放入 type 中Integer。第二行f44通过模式匹配 unit 的唯一构造函数来定义,即(),并生成数字 44。您可以通过提供单位值 来调用此函数():
The first line declares that f44 takes the type (), pronounced “unit,” into the type Integer. The second line defines f44 by pattern matching the only constructor for unit, namely (), and producing the number 44. You call this function by providing the unit value ():
f44 ()f44 ()
请注意,unit 的每个函数都相当于从目标类型中选取单个元素(此处选取 44 Integer)。事实上,您可以将其视为f44数字 44 的不同表示形式。这是一个示例,说明我们如何通过讨论函数(箭头)来替换对集合元素的明确提及。从单元到任何类型 A 的函数都与该集合 A 的元素一一对应。
Notice that every function of unit is equivalent to picking a single element from the target type (here, picking the Integer 44). In fact you could think of f44 as a different representation for the number 44. This is an example of how we can replace explicit mention of elements of a set by talking about functions (arrows) instead. Functions from unit to any type A are in one-to-one correspondence with the elements of that set A.
void具有返回类型的函数,或者在 Haskell 中,具有单位返回类型的函数又如何呢?在 C++ 中,此类函数用于产生副作用,但我们知道这些函数并不是数学意义上的真正函数。返回单位的纯函数不执行任何操作:它丢弃其参数。
What about functions with the void return type, or, in Haskell, with the unit return type? In C++ such functions are used for side effects, but we know that these are not real functions in the mathematical sense of the word. A pure function that returns unit does nothing: it discards its argument.
从数学上讲,从集合 A 到单例集的函数将 A 的每个元素映射到该单例集的单个元素。对于每一个 A 都只有一个这样的函数。这是这个函数Integer:
Mathematically, a function from a set A to a singleton set maps every element of A to the single element of that singleton set. For every A there is exactly one such function. Here’s this function for Integer:
fInt :: Integer -> ()
fInt x = ()fInt :: Integer -> ()
fInt x = ()
你给它任何整数,它就会返回一个单位。本着简洁的精神,Haskell 允许您使用通配符模式(下划线)来表示被丢弃的参数。这样您就不必为它发明一个名称。所以上式可以改写为:
You give it any integer, and it gives you back a unit. In the spirit of terseness, Haskell lets you use the wildcard pattern, the underscore, for an argument that is discarded. This way you don’t have to invent a name for it. So the above can be rewritten as:
fInt :: Integer -> ()
fInt _ = ()fInt :: Integer -> ()
fInt _ = ()
请注意,该函数的实现不仅不依赖于传递给它的值,而且甚至不依赖于参数的类型。
Notice that the implementation of this function not only doesn’t depend on the value passed to it, but it doesn’t even depend on the type of the argument.
对于任何类型都可以使用相同公式实现的函数称为参数多态。您可以使用类型参数而不是具体类型,通过一个方程来实现一整套此类函数。我们应该如何称呼从任意类型到单元类型的多态函数?当然我们会称它为unit:
Functions that can be implemented with the same formula for any type are called parametrically polymorphic. You can implement a whole family of such functions with one equation using a type parameter instead of a concrete type. What should we call a polymorphic function from any type to unit type? Of course we’ll call it unit:
unit :: a -> ()
unit _ = ()unit :: a -> ()
unit _ = ()
在 C++ 中,您可以将此函数编写为:
In C++ you would write this function as:
template
void unit(T) {}template
void unit(T) {}
类型分类中的下一个是二元素集。在 C++ 中,它被称为bool,而在 Haskell 中,可以预见,它被称为Bool. 不同之处在于,在 C++ 中bool是内置类型,而在 Haskell 中可以定义如下:
Next in the typology of types is a two-element set. In C++ it’s called bool and in Haskell, predictably, Bool. The difference is that in C++ bool is a built-in type, whereas in Haskell it can be defined as follows:
data Bool = True | Falsedata Bool = True | False
(阅读该定义的方式是Bool或True。False)原则上,我们还应该能够在 C++ 中将布尔类型定义为枚举:
(The way to read this definition is that Bool is either True or False.) In principle, one should also be able to define a Boolean type in C++ as an enumeration:
enum bool {
true,
false
};enum bool {
true,
false
};
但C++enum秘密地是一个整数。可以使用C++11 “ enum class” 来代替,但随后您必须使用类名来限定其值,如 和 中bool::true,bool::false更不用说必须在使用它的每个文件中包含适当的标头。
but C++ enum is secretly an integer. The C++11 “enum class” could have been used instead, but then you would have to qualify its values with the class name, as in bool::true and bool::false, not to mention having to include the appropriate header in every file that uses it.
纯函数Bool只需从目标类型中选取两个值,一个对应于True,另一个对应于False。
Pure functions from Bool just pick two values from the target type, one corresponding to True and another to False.
函数 toBool称为谓词。例如,Haskell 库Data.Char充满了像isAlphaor之类的谓词isDigit。在 C++ 中,有一个类似的库,其中定义了isalpha和isdigit,但它们返回一个int而不是布尔值。实际谓词在 中定义,并具有、等std::ctype形式。ctype::is(alpha, c)ctype::is(digit, c)
Functions to Bool are called predicates. For instance, the Haskell library Data.Char is full of predicates like isAlpha or isDigit. In C++ there is a similar library that defines, among others, isalpha and isdigit, but these return an int rather than a Boolean. The actual predicates are defined in std::ctype and have the form ctype::is(alpha, c), ctype::is(digit, c), etc.
memoize用您喜欢的语言定义一个高阶函数(或函数对象) 。该函数采用纯函数f作为参数,并返回一个行为几乎与 完全相同的函数f,不同之处在于它只为每个参数调用一次原始函数,在内部存储结果,随后每次使用相同的值调用时都返回此存储的结果争论。您可以通过观察其性能来区分记忆的函数和原始函数。例如,尝试记住一个需要很长时间才能计算的函数。第一次调用它时,您必须等待结果,但在后续调用中,使用相同的参数,您应该立即获得结果。memoize in your favorite language. This function takes a pure function f as an argument and returns a function that behaves almost exactly like f, except that it only calls the original function once for every argument, stores the result internally, and subsequently returns this stored result every time it’s called with the same argument. You can tell the memoized function from the original by watching its performance. For instance, try to memoize a function that takes a long time to evaluate. You’ll have to wait for the result the first time you call it, but on subsequent calls, with the same argument, you should get the result immediately.std::getchar()bool f()
{
std::cout << "Hello!" << std::endl;
return true;
}int f(int x)
{
static int y = 0;
y += x;
return y;
}std::getchar()bool f()
{
std::cout << "Hello!" << std::endl;
return true;
}int f(int x)
{
static int y = 0;
y += x;
return y;
}Bool从到共有多少种不同的函数Bool?你能全部实现吗?Bool to Bool? Can you implement them all?Void、()(单位)和Bool;箭头对应于这些类型之间所有可能的功能。用函数名称标记箭头。Void, () (unit), and Bool; with arrows corresponding to all possible functions between these types. Label the arrows with the names of the functions.通过研究各种示例,您可以真正了解范畴。范畴有各种形状和大小,并且经常出现在意想不到的地方。我们将从一些非常简单的事情开始。
You can get real appreciation for categories by studying a variety of examples. Categories come in all shapes and sizes and often pop up in unexpected places. We’ll start with something really simple.
最平凡的范畴是零个对象,因此零态射的范畴。它本身是一个非常悲伤的范畴,但在其他范畴的背景下,例如在所有范畴的范畴中(是的,有一个),它可能很重要。如果您认为空集有意义,那么为什么不是空范畴呢?
The most trivial category is one with zero objects and, consequently, zero morphisms. It’s a very sad category by itself, but it may be important in the context of other categories, for instance, in the category of all categories (yes, there is one). If you think that an empty set makes sense, then why not an empty category?
您只需通过用箭头连接对象即可构建范畴。您可以想象从任何有向图开始,只需添加更多箭头即可将其变成一个范畴。首先,在每个节点添加一个恒等箭头。然后,对于任意两个箭头,其中一个箭头的结尾与另一个箭头的开头重合(换句话说,任意两个可组合箭头),添加一个新箭头作为它们的组合。每次添加新箭头时,您还必须考虑它与任何其他箭头(单位箭头除外)及其本身的组合。通常你最终会得到无限多个箭头,但这没关系。
You can build categories just by connecting objects with arrows. You can imagine starting with any directed graph and making it into a category by simply adding more arrows. First, add an identity arrow at each node. Then, for any two arrows such that the end of one coincides with the beginning of the other (in other words, any two composable arrows), add a new arrow to serve as their composition. Every time you add a new arrow, you have to also consider its composition with any other arrow (except for the identity arrows) and itself. You usually end up with infinitely many arrows, but that’s okay.
查看此过程的另一种方式是,您正在创建一个范畴,该范畴为图中的每个节点都有一个对象,以及作为态射的所有可能的可组合图边链。(您甚至可以将恒等态射视为长度为零的链的特殊情况。)
Another way of looking at this process is that you’re creating a category, which has an object for every node in the graph, and all possible chains of composable graph edges as morphisms. (You may even consider identity morphisms as special cases of chains of length zero.)
这样的范畴称为由给定图生成的自由范畴。这是自由构造的一个示例,即通过用最少数量的项扩展来完成给定结构以满足其定律(此处为范畴定律)的过程。将来我们会看到更多这样的例子。
Such a category is called a free category generated by a given graph. It’s an example of a free construction, a process of completing a given structure by extending it with a minimum number of items to satisfy its laws (here, the laws of a category). We’ll see more examples of it in the future.
现在我们来看看完全不同的东西!态射是对象之间的关系的范畴:小于或等于的关系。让我们检查一下它是否确实是一个范畴。我们有恒等态射吗?每个对象都小于或等于自身:检查!我们有组合吗?如果 a <= b 且 b <= c 则 a <= c:检查!组合是否具有结合性?查看!具有这样关系的集合称为前序,因此前序确实是一个范畴。
And now for something completely different! A category where a morphism is a relation between objects: the relation of being less than or equal. Let’s check if it indeed is a category. Do we have identity morphisms? Every object is less than or equal to itself: check! Do we have composition? If a <= b and b <= c then a <= c: check! Is composition associative? Check! A set with a relation like this is called a preorder, so a preorder is indeed a category.
您还可以有一个更强的关系,它满足一个附加条件,即如果 a <= b 且 b <= a,则 a 必须与 b 相同。这就是所谓的偏序。
You can also have a stronger relation, that satisfies an additional condition that, if a <= b and b <= a then a must be the same as b. That’s called a partial order.
最后,您可以强加一个条件,即任何两个对象都以某种方式相互关联;这给了你一个线性顺序或全序。
Finally, you can impose the condition that any two objects are in a relation with each other, one way or another; and that gives you a linear order or total order.
让我们将这些有序集描述为范畴。前序是一个范畴,其中从任何对象 a 到任何对象 b 至多有一个态射。这种范畴的另一个名称是“薄”。预购是一个很薄弱的范畴。
Let’s characterize these ordered sets as categories. A preorder is a category where there is at most one morphism going from any object a to any object b. Another name for such a category is “thin.” A preorder is a thin category.
范畴 C 中从对象 a 到对象 b 的态射集称为hom 集,写为 C(a, b)(有时也写为 Hom C (a, b))。因此,预购中的每个 hom-set 要么是空的,要么是单例的。这包括 hom 集 C(a, a),即从 a 到 a 的态射集,它必须是单例,仅包含任何前序中的恒等式。但是,您可以预订周期。部分顺序禁止循环。
A set of morphisms from object a to object b in a category C is called a hom-set and is written as C(a, b) (or, sometimes, HomC(a, b)). So every hom-set in a preorder is either empty or a singleton. That includes the hom-set C(a, a), the set of morphisms from a to a, which must be a singleton, containing only the identity, in any preorder. You may, however, have cycles in a preorder. Cycles are forbidden in a partial order.
由于排序,能够识别预序、部分序和全序非常重要。排序算法,例如快速排序、冒泡排序、合并排序等,只能在总订单上正确工作。可以使用拓扑排序来对偏序进行排序。
It’s very important to be able to recognize preorders, partial orders, and total orders because of sorting. Sorting algorithms, such as quicksort, bubble sort, merge sort, etc., can only work correctly on total orders. Partial orders can be sorted using topological sort.
Monoid 是一个简单得令人尴尬但强大得惊人的概念。这是基本算术背后的概念:加法和乘法都形成一个幺半群。幺半群在编程中无处不在。它们以字符串、列表、可折叠数据结构、并发编程中的 future、函数反应式编程中的事件等形式出现。
Monoid is an embarrassingly simple but amazingly powerful concept. It’s the concept behind basic arithmetics: Both addition and multiplication form a monoid. Monoids are ubiquitous in programming. They show up as strings, lists, foldable data structures, futures in concurrent programming, events in functional reactive programming, and so on.
传统上,幺半群被定义为具有二元运算的集合。此操作所需的只是它是关联的,并且有一个特殊元素的行为类似于它的一个单元。
Traditionally, a monoid is defined as a set with a binary operation. All that’s required from this operation is that it’s associative, and that there is one special element that behaves like a unit with respect to it.
例如,带有零的自然数在加法下形成幺半群。结合性意味着:
For instance, natural numbers with zero form a monoid under addition. Associativity means that:
(a + b) + c = a + (b + c)(a + b) + c = a + (b + c)
(换句话说,我们在添加数字时可以跳过括号。)
(In other words, we can skip parentheses when adding numbers.)
中性元素为零,因为:
The neutral element is zero, because:
0 + a = a0 + a = a
和
and
a + 0 = aa + 0 = a
第二个方程是多余的,因为加法是可交换的 (a + b = b + a),但交换性不是幺半群定义的一部分。例如,字符串连接是不可交换的,但它形成了一个幺半群。顺便说一下,字符串连接的中性元素是一个空字符串,它可以附加到字符串的任一侧而不改变它。
The second equation is redundant, because addition is commutative (a + b = b + a), but commutativity is not part of the definition of a monoid. For instance, string concatenation is not commutative and yet it forms a monoid. The neutral element for string concatenation, by the way, is an empty string, which can be attached to either side of a string without changing it.
在 Haskell 中,我们可以为幺半群定义一个类型类——该类型有一个名为 的中性元素mempty和一个名为 的二元运算mappend:
In Haskell we can define a type class for monoids — a type for which there is a neutral element called mempty and a binary operation called mappend:
class Monoid m where
mempty :: m
mappend :: m -> m -> mclass Monoid m where
mempty :: m
mappend :: m -> m -> m
双参数函数的类型签名一m->m->m开始可能看起来很奇怪,但在我们讨论柯里化之后它就会变得非常有意义。您可以通过两种基本方式解释具有多个箭头的签名:作为多个参数的函数,最右边的类型是返回类型;或者作为一个参数(最左边的参数)的函数,返回一个函数。可以通过添加括号(这是多余的,因为箭头是右结合的)来强调后一种解释,如:m->(m->m)。我们稍后会回到这个解释。
The type signature for a two-argument function, m->m->m, might look strange at first, but it will make perfect sense after we talk about currying. You may interpret a signature with multiple arrows in two basic ways: as a function of multiple arguments, with the rightmost type being the return type; or as a function of one argument (the leftmost one), returning a function. The latter interpretation may be emphasized by adding parentheses (which are redundant, because the arrow is right-associative), as in: m->(m->m). We’ll come back to this interpretation in a moment.
请注意,在 Haskell 中,无法表达mempty和的幺半群属性(即中性且结合的mappend事实)。确保他们满意是程序员的责任。memptymappend
Notice that, in Haskell, there is no way to express the monoidal properties of mempty and mappend (i.e., the fact that mempty is neutral and that mappend is associative). It’s the responsibility of the programmer to make sure they are satisfied.
Haskell 类不像 C++ 类那样具有侵入性。当您定义新类型时,不必预先指定其类。您可以自由地拖延并稍后将给定类型声明为某个类的实例。作为一个例子,让我们通过提供andString的实现来声明自己是一个幺半群(事实上,这是在标准 Prelude 中为您完成的):memptymappend
Haskell classes are not as intrusive as C++ classes. When you’re defining a new type, you don’t have to specify its class up front. You are free to procrastinate and declare a given type to be an instance of some class much later. As an example, let’s declare String to be a monoid by providing the implementation of mempty and mappend (this is, in fact, done for you in the standard Prelude):
instance Monoid String where
mempty = ""
mappend = (++)instance Monoid String where
mempty = ""
mappend = (++)
在这里,我们重用了列表连接运算符(++),因为 aString只是一个字符列表。
Here, we have reused the list concatenation operator (++), because a String is just a list of characters.
关于 Haskell 语法的一句话:任何中缀运算符都可以通过用括号括起来而变成双参数函数。给定两个字符串,您可以通过在它们之间插入来连接它们++:
A word about Haskell syntax: Any infix operator can be turned into a two-argument function by surrounding it with parentheses. Given two strings, you can concatenate them by inserting ++ between them:
"Hello " ++ "world!""Hello " ++ "world!"
或者将它们作为两个参数传递给括号(++):
or by passing them as two arguments to the parenthesized (++):
(++) "Hello " "world!"(++) "Hello " "world!"
请注意,函数的参数不是用逗号分隔或用括号括起来。(这可能是学习 Haskell 时最难适应的事情。)
Notice that arguments to a function are not separated by commas or surrounded by parentheses. (This is probably the hardest thing to get used to when learning Haskell.)
值得强调的是,Haskell 允许您表达函数的相等性,如下所示:
It’s worth emphasizing that Haskell lets you express equality of functions, as in:
mappend = (++)mappend = (++)
从概念上讲,这与表达函数产生的值的相等性不同,如下所示:
Conceptually, this is different than expressing the equality of values produced by functions, as in:
mappend s1 s2 = (++) s1 s2mappend s1 s2 = (++) s1 s2
前者转化为Hask范畴中的态射相等(或者Set,如果我们忽略底部,这是永无止境的计算的名称)。这样的方程不仅更简洁,而且通常可以推广到其他范畴。后者称为外延相等,它指出了这样一个事实:对于任何两个输入字符串,mappend和 的输出(++)都是相同的。由于参数的值有时称为点(如:f 在点 x 处的值),因此这称为逐点相等。不指定参数的函数相等被描述为point-free。(顺便说一句,无点方程通常涉及函数的组合,这些函数由点表示,因此这可能会让初学者有点困惑。)
The former translates into equality of morphisms in the category Hask (or Set, if we ignore bottoms, which is the name for never-ending calculations). Such equations are not only more succinct, but can often be generalized to other categories. The latter is called extensional equality, and states the fact that for any two input strings, the outputs of mappend and (++) are the same. Since the values of arguments are sometimes called points (as in: the value of f at point x), this is called point-wise equality. Function equality without specifying the arguments is described as point-free. (Incidentally, point-free equations often involve composition of functions, which is symbolized by a point, so this might be a little confusing to the beginner.)
在 C++ 中声明幺半群最接近的方法是使用(建议的)概念语法。
The closest one can get to declaring a monoid in C++ would be to use the (proposed) syntax for concepts.
template
T mempty = delete;
template
T mappend(T, T) = delete;
template
concept bool Monoid = requires (M m) {
{ mempty } -> M;
{ mappend(m, m); } -> M;
};template
T mempty = delete;
template
T mappend(T, T) = delete;
template
concept bool Monoid = requires (M m) {
{ mempty } -> M;
{ mappend(m, m); } -> M;
};
第一个定义使用值模板(也已提议)。多态值是一个值族——每种类型都有不同的值。
The first definition uses a value template (also proposed). A polymorphic value is a family of values — a different value for every type.
该关键字delete意味着没有定义默认值:必须根据具体情况进行指定。同样,没有默认值mappend。
The keyword delete means that there is no default value defined: It will have to be specified on a case-by-case basis. Similarly, there is no default for mappend.
这个概念Monoid是一个谓词(因此是类型),用于测试给定类型和bool是否存在适当的定义。memptymappendM
The concept Monoid is a predicate (hence the bool type) that tests whether there exist appropriate definitions of mempty and mappend for a given type M.
Monoid 概念的实例化可以通过提供适当的特化和重载来完成:
An instantiation of the Monoid concept can be accomplished by providing appropriate specializations and overloads:
template<>
std::string mempty = {""};
std::string mappend(std::string s1, std::string s2) {
return s1 + s2;
}template<>
std::string mempty = {""};
std::string mappend(std::string s1, std::string s2) {
return s1 + s2;
}
这是对集合元素的幺半群的“熟悉”定义。但正如你所知,在范畴论中,我们试图摆脱集合及其元素,而是谈论对象和态射。因此,让我们稍微改变一下我们的观点,将二元运算符的应用视为围绕集合“移动”或“移动”事物。
That was the “familiar” definition of the monoid in terms of elements of a set. But as you know, in category theory we try to get away from sets and their elements, and instead talk about objects and morphisms. So let’s change our perspective a bit and think of the application of the binary operator as “moving” or “shifting” things around the set.
例如,有每个自然数加5的运算。它将 0 映射到 5、1 到 6、2 到 7,依此类推。这是在自然数集上定义的函数。这很好:我们有一个函数和一个集合。一般来说,对于任何数字 n,都有一个将 n 加起来的函数——n 的“加法器”。
For instance, there is the operation of adding 5 to every natural number. It maps 0 to 5, 1 to 6, 2 to 7, and so on. That’s a function defined on the set of natural numbers. That’s good: we have a function and a set. In general, for any number n there is a function of adding n — the “adder” of n.
加法器是如何组成的?加5的函数与加7的函数的复合是加12的函数。因此加法器的复合可以等效于加法规则。这也很好:我们可以用函数组合代替加法。
How do adders compose? The composition of the function that adds 5 with the function that adds 7 is a function that adds 12. So the composition of adders can be made equivalent to the rules of addition. That’s good too: we can replace addition with function composition.
但是等等,还有更多:还有中性元素零的加法器。加零不会改变事物的位置,因此它是自然数集合中的恒等函数。
But wait, there’s more: There is also the adder for the neutral element, zero. Adding zero doesn’t move things around, so it’s the identity function in the set of natural numbers.
我不给你传统的加法规则,而是给你组合加法器的规则,而不会丢失任何信息。请注意,加法器的组合是结合的,因为函数的组合是结合的;我们有与恒等函数相对应的零加法器。
Instead of giving you the traditional rules of addition, I could as well give you the rules of composing adders, without any loss of information. Notice that the composition of adders is associative, because the composition of functions is associative; and we have the zero adder corresponding to the identity function.
精明的读者可能已经注意到,从整数到加法器的映射遵循mappendas类型签名的第二种解释m->(m->m)。它告诉我们将mappend幺半群集合的元素映射到作用于该集合的函数。
An astute reader might have noticed that the mapping from integers to adders follows from the second interpretation of the type signature of mappend as m->(m->m). It tells us that mappend maps an element of a monoid set to a function acting on that set.
现在我希望你忘记你正在处理一组自然数,而只是将其视为一个单一的对象,一个带有一堆态射的斑点——加法器。幺半群是单个对象范畴。事实上,monoid 这个名字来自希腊语mono,意思是单一。每个幺半群都可以被描述为具有一组遵循适当的组合规则的态射的单个对象范畴。
Now I want you to forget that you are dealing with the set of natural numbers and just think of it as a single object, a blob with a bunch of morphisms — the adders. A monoid is a single object category. In fact the name monoid comes from Greek mono, which means single. Every monoid can be described as a single object category with a set of morphisms that follow appropriate rules of composition.
字符串连接是一个有趣的例子,因为我们可以选择定义右追加器和左追加器(或者prependers,如果你愿意的话)。两个模型的成分表互为镜像相反。您可以轻松地说服自己,在“foo”之后附加“bar”相当于在“bar”之后添加“foo”。
String concatenation is an interesting case, because we have a choice of defining right appenders and left appenders (or prependers, if you will). The composition tables of the two models are a mirror reverse of each other. You can easily convince yourself that appending “bar” after “foo” corresponds to prepending “foo” after prepending “bar”.
您可能会问,是否每个分类幺半群(单对象范畴)都定义了一个唯一的带有二元运算符集合的幺半群。事实证明,我们总是可以从单个对象范畴中提取一个集合。这个集合是态射的集合——我们例子中的加法器。换句话说,我们有范畴 M 中单个对象 m 的 hom 集 M(m, m)。我们可以轻松地在这个集合中定义一个二元运算符:两个集合元素的幺半积就是对应于相应态射的组合。如果你给我 M(m, m) 中对应于f和的两个元素g,它们的乘积将对应于组合g∘f。组合始终存在,因为这些态射的源和目标是同一个对象。并且它是按照范畴规则关联的。恒等态射是该乘积的中性元素。所以我们总是可以从范畴幺半群中恢复集合幺半群。就所有意图和目的而言,它们都是一样的。
You might ask the question whether every categorical monoid — a one-object category — defines a unique set-with-binary-operator monoid. It turns out that we can always extract a set from a single-object category. This set is the set of morphisms — the adders in our example. In other words, we have the hom-set M(m, m) of the single object m in the category M. We can easily define a binary operator in this set: The monoidal product of two set-elements is the element corresponding to the composition of the corresponding morphisms. If you give me two elements of M(m, m) corresponding to f and g, their product will correspond to the composition g∘f. The composition always exists, because the source and the target for these morphisms are the same object. And it’s associative by the rules of category. The identity morphism is the neutral element of this product. So we can always recover a set monoid from a category monoid. For all intents and purposes they are one and the same.
幺半群 hom 集被视为态射和集合中的点
Monoid hom-set seen as morphisms and as points in a set
对于数学家来说,只有一个小问题需要注意:态射不必形成一个集合。在范畴的世界里,有比集合更大的东西。任意两个对象之间的态射形成集合的范畴称为局部小范畴。正如所承诺的,我基本上会忽略这些微妙之处,但我认为我应该提及它们以记录在案。
There is just one little nit for mathematicians to pick: morphisms don’t have to form a set. In the world of categories there are things larger than sets. A category in which morphisms between any two objects form a set is called locally small. As promised, I will be mostly ignoring such subtleties, but I thought I should mention them for the record.
范畴论中许多有趣的现象都源于这样一个事实:hom 集的元素既可以看作遵循组合规则的态射,也可以看作集合中的点。这里,M 中态射的复合转化为集合 M(m, m) 中的幺半群积。
A lot of interesting phenomena in category theory have their root in the fact that elements of a hom-set can be seen both as morphisms, which follow the rules of composition, and as points in a set. Here, composition of morphisms in M translates into monoidal product in the set M(m, m).
我要感谢 Andrew Sutton 根据他和 Bjarne Stroustrup 的最新提案重写了我的 C++ 幺半群概念代码。
I’d like to thank Andrew Sutton for rewriting my C++ monoid concept code according to his and Bjarne Stroustrup’s latest proposal.
&&考虑到 Bool 是两个值 True 和 False 的集合,表明它分别相对于运算符(AND) 和||(OR)形成两个(集合论)幺半群。&& (AND) and || (OR).您已经了解了如何将类型和纯函数建模为范畴。我还提到,有一种方法可以在范畴论中对副作用或非纯函数进行建模。让我们看一个这样的例子:记录或跟踪其执行的函数。在命令式语言中,可能会通过改变某些全局状态来实现,例如:
You’ve seen how to model types and pure functions as a category. I also mentioned that there is a way to model side effects, or non-pure functions, in category theory. Let’s have a look at one such example: functions that log or trace their execution. Something that, in an imperative language, would likely be implemented by mutating some global state, as in:
string logger;
bool negate(bool b) {
logger += "Not so! ";
return !b;
}string logger;
bool negate(bool b) {
logger += "Not so! ";
return !b;
}
您知道这不是一个纯函数,因为它的记忆版本将无法生成日志。这个功能有副作用。
You know that this is not a pure function, because its memoized version would fail to produce a log. This function has side effects.
在现代编程中,我们尝试尽可能远离全局可变状态——即使只是因为并发的复杂性。而且你永远不会将这样的代码放入库中。
In modern programming, we try to stay away from global mutable state as much as possible — if only because of the complications of concurrency. And you would never put code like this in a library.
对我们来说幸运的是,可以使这个函数变得纯粹。您只需明确地传入和传出日志即可。让我们通过添加一个字符串参数,并将常规输出与包含更新日志的字符串配对来做到这一点:
Fortunately for us, it’s possible to make this function pure. You just have to pass the log explicitly, in and out. Let’s do that by adding a string argument, and pairing regular output with a string that contains the updated log:
pair<bool, string> negate(bool b, string logger) {
return make_pair(!b, logger + "Not so! ");
}pair<bool, string> negate(bool b, string logger) {
return make_pair(!b, logger + "Not so! ");
}
这个函数是纯粹的,没有副作用,每次使用相同的参数调用时都会返回相同的对,并且如果需要的话可以记忆。但是,考虑到日志的累积性质,您必须记住可能导致给定调用的所有可能的历史记录。将会有一个单独的备忘录条目:
This function is pure, it has no side effects, it returns the same pair every time it’s called with the same arguments, and it can be memoized if necessary. However, considering the cumulative nature of the log, you’d have to memoize all possible histories that can lead to a given call. There would be a separate memo entry for:
negate(true, "It was the best of times. ");negate(true, "It was the best of times. ");
和
and
negate(true, "It was the worst of times. ");negate(true, "It was the worst of times. ");
等等。
and so on.
对于库函数来说它也不是一个很好的接口。调用者可以自由地忽略返回类型中的字符串,因此这并不是一个巨大的负担;但他们被迫传递一个字符串作为输入,这可能不方便。
It’s also not a very good interface for a library function. The callers are free to ignore the string in the return type, so that’s not a huge burden; but they are forced to pass a string as input, which might be inconvenient.
有没有一种方法可以减少干扰地做同样的事情?有没有办法分离关注点?在这个简单的示例中,该函数的主要目的negate是将一个布尔值转换为另一个布尔值。日志记录是次要的。当然,记录的消息是特定于该函数的,但将消息聚合到一个连续日志中的任务是一个单独的问题。我们仍然希望该函数生成一个字符串,但我们希望减轻它生成日志的负担。所以这是折衷的解决方案:
Is there a way to do the same thing less intrusively? Is there a way to separate concerns? In this simple example, the main purpose of the function negate is to turn one Boolean into another. The logging is secondary. Granted, the message that is logged is specific to the function, but the task of aggregating the messages into one continuous log is a separate concern. We still want the function to produce a string, but we’d like to unburden it from producing a log. So here’s the compromise solution:
pair<bool, string> negate(bool b) {
return make_pair(!b, "Not so! ");
}pair<bool, string> negate(bool b) {
return make_pair(!b, "Not so! ");
}
这个想法是日志将在函数调用之间聚合。
The idea is that the log will be aggregated between function calls.
为了了解如何做到这一点,让我们切换到一个稍微更实际的示例。我们有一个从字符串到字符串的函数,可以将小写字符转换为大写字符:
To see how this can be done, let’s switch to a slightly more realistic example. We have one function from string to string that turns lower case characters to upper case:
string toUpper(string s) {
string result;
int (*toupperp)(int) = &toupper; // toupper is overloaded
transform(begin(s), end(s), back_inserter(result), toupperp);
return result;
}string toUpper(string s) {
string result;
int (*toupperp)(int) = &toupper; // toupper is overloaded
transform(begin(s), end(s), back_inserter(result), toupperp);
return result;
}
另一个将字符串拆分为字符串向量,并在空白边界处将其打破:
and another that splits a string into a vector of strings, breaking it on whitespace boundaries:
vector<string> toWords(string s) {
return words(s);
}vector<string> toWords(string s) {
return words(s);
}
实际工作是在辅助函数中完成的words:
The actual work is done in the auxiliary function words:
vector<string> words(string s) {
vector<string> result{""};
for (auto i = begin(s); i != end(s); ++i)
{
if (isspace(*i))
result.push_back("");
else
result.back() += *i;
}
return result;
}vector<string> words(string s) {
vector<string> result{""};
for (auto i = begin(s); i != end(s); ++i)
{
if (isspace(*i))
result.push_back("");
else
result.back() += *i;
}
return result;
}
我们想要修改这些函数toUpper,toWords以便它们在常规返回值之上搭载消息字符串。
We want to modify the functions toUpper and toWords so that they piggyback a message string on top of their regular return values.
我们将“修饰”这些函数的返回值。让我们通过定义一个模板来以通用方式完成此操作,Writer该模板封装一对,其第一个组件是任意类型的值A,第二个组件是字符串:
We will “embellish” the return values of these functions. Let’s do it in a generic way by defining a template Writer that encapsulates a pair whose first component is a value of arbitrary type A and the second component is a string:
template<class A>
using Writer = pair<A, string>;template<class A>
using Writer = pair<A, string>;
下面是修饰后的函数:
Here are the embellished functions:
Writer<string> toUpper(string s) {
string result;
int (*toupperp)(int) = &toupper;
transform(begin(s), end(s), back_inserter(result), toupperp);
return make_pair(result, "toUpper ");
}
Writer<vector<string>> toWords(string s) {
return make_pair(words(s), "toWords ");
}Writer<string> toUpper(string s) {
string result;
int (*toupperp)(int) = &toupper;
transform(begin(s), end(s), back_inserter(result), toupperp);
return make_pair(result, "toUpper ");
}
Writer<vector<string>> toWords(string s) {
return make_pair(words(s), "toWords ");
}
我们希望将这两个函数组合成另一个修饰函数,该函数将字符串大写并将其拆分为单词,同时生成这些操作的日志。我们可以这样做:
We want to compose these two functions into another embellished function that uppercases a string and splits it into words, all the while producing a log of those actions. Here’s how we may do it:
Writer<vector<string>> process(string s) {
auto p1 = toUpper(s);
auto p2 = toWords(p1.first);
return make_pair(p2.first, p1.second + p2.second);
}Writer<vector<string>> process(string s) {
auto p1 = toUpper(s);
auto p2 = toWords(p1.first);
return make_pair(p2.first, p1.second + p2.second);
}
我们已经实现了我们的目标:日志的聚合不再是各个函数的关注点。它们生成自己的消息,然后在外部将其连接成更大的日志。
We have accomplished our goal: The aggregation of the log is no longer the concern of the individual functions. They produce their own messages, which are then, externally, concatenated into a larger log.
现在想象一下以这种风格编写的整个程序。这是重复且容易出错的代码的噩梦。但我们是程序员。我们知道如何处理重复的代码:我们将其抽象!然而,这不是一般的抽象——我们必须抽象函数组合本身。但组合是范畴论的本质,所以在我们编写更多代码之前,让我们从范畴的角度来分析问题。
Now imagine a whole program written in this style. It’s a nightmare of repetitive, error-prone code. But we are programmers. We know how to deal with repetitive code: we abstract it! This is, however, not your run of the mill abstraction — we have to abstract function composition itself. But composition is the essence of category theory, so before we write more code, let’s analyze the problem from the categorical point of view.
修饰一堆函数的返回类型以搭载一些附加功能的想法被证明是非常富有成效的。我们将会看到更多这样的例子。起点是我们常规的类型和函数范畴。我们将把类型保留为对象,但将态射重新定义为修饰函数。
The idea of embellishing the return types of a bunch of functions in order to piggyback some additional functionality turns out to be very fruitful. We’ll see many more examples of it. The starting point is our regular category of types and functions. We’ll leave the types as objects, but redefine our morphisms to be the embellished functions.
例如,假设我们想要修饰isEven从int到 的函数bool。我们将其转换为由修饰函数表示的态射。重要的一点是,这个态射仍然被认为是对象int和之间的箭头bool,即使修饰函数返回一对:
For instance, suppose that we want to embellish the function isEven that goes from int to bool. We turn it into a morphism that is represented by an embellished function. The important point is that this morphism is still considered an arrow between the objects int and bool, even though the embellished function returns a pair:
pair<bool, string> isEven(int n) {
return make_pair(n % 2 == 0, "isEven ");
}pair<bool, string> isEven(int n) {
return make_pair(n % 2 == 0, "isEven ");
}
根据范畴定律,我们应该能够将这个态射与另一个从对象bool到任何东西的态射组合起来。特别是,我们应该能够将它与我们之前的组合起来negate:
By the laws of a category, we should be able to compose this morphism with another morphism that goes from the object bool to whatever. In particular, we should be able to compose it with our earlier negate:
pair<bool, string> negate(bool b) {
return make_pair(!b, "Not so! ");
}pair<bool, string> negate(bool b) {
return make_pair(!b, "Not so! ");
}
显然,由于输入/输出不匹配,我们不能像构造常规函数那样构造这两个态射。他们的组成应该看起来更像这样:
Obviously, we cannot compose these two morphisms the same way we compose regular functions, because of the input/output mismatch. Their composition should look more like this:
pair<bool, string> isOdd(int n) {
pair<bool, string> p1 = isEven(n);
pair<bool, string> p2 = negate(p1.first);
return make_pair(p2.first, p1.second + p2.second);
}pair<bool, string> isOdd(int n) {
pair<bool, string> p1 = isEven(n);
pair<bool, string> p2 = negate(p1.first);
return make_pair(p2.first, p1.second + p2.second);
}
因此,这是我们正在构建的这个新范畴中两个态射的组合的秘诀:
So here’s the recipe for the composition of two morphisms in this new category we are constructing:
如果我们想将此组合抽象为 C++ 中的高阶函数,则必须使用由与范畴中的三个对象相对应的三种类型参数化的模板。它应该采用两个根据我们的规则可组合的修饰函数,并返回第三个修饰函数:
If we want to abstract this composition as a higher order function in C++, we have to use a template parameterized by three types corresponding to three objects in our category. It should take two embellished functions that are composable according to our rules, and return a third embellished function:
template<class A, class B, class C>
function<Writer<C>(A)> compose(function<Writer<B>(A)> m1,
function<Writer<C>(B)> m2)
{
return [m1, m2](A x) {
auto p1 = m1(x);
auto p2 = m2(p1.first);
return make_pair(p2.first, p1.second + p2.second);
};
}template<class A, class B, class C>
function<Writer<C>(A)> compose(function<Writer<B>(A)> m1,
function<Writer<C>(B)> m2)
{
return [m1, m2](A x) {
auto p1 = m1(x);
auto p2 = m2(p1.first);
return make_pair(p2.first, p1.second + p2.second);
};
}
现在我们可以回到之前的示例并实现这个新模板的组合toUpper和使用:toWords
Now we can go back to our earlier example and implement the composition of toUpper and toWords using this new template:
Writer<vector<string>> process(string s) {
return compose<string, string, vector<string>>(toUpper, toWords)(s);
}Writer<vector<string>> process(string s) {
return compose<string, string, vector<string>>(toUpper, toWords)(s);
}
将类型传递给模板仍然存在很多噪音compose。只要您有一个兼容 C++14 的编译器,支持带有返回类型推导的广义 lambda 函数,就可以避免这种情况(此代码归功于 Eric Niebler):
There is still a lot of noise with the passing of types to the compose template. This can be avoided as long as you have a C++14-compliant compiler that supports generalized lambda functions with return type deduction (credit for this code goes to Eric Niebler):
auto const compose = [](auto m1, auto m2) {
return [m1, m2](auto x) {
auto p1 = m1(x);
auto p2 = m2(p1.first);
return make_pair(p2.first, p1.second + p2.second);
};
};auto const compose = [](auto m1, auto m2) {
return [m1, m2](auto x) {
auto p1 = m1(x);
auto p2 = m2(p1.first);
return make_pair(p2.first, p1.second + p2.second);
};
};
在这个新定义中, 的实现process简化为:
In this new definition, the implementation of process simplifies to:
Writer<vector<string>> process(string s){
return compose(toUpper, toWords)(s);
}Writer<vector<string>> process(string s){
return compose(toUpper, toWords)(s);
}
但我们还没有完成。我们已经在新范畴中定义了组合,但是恒等态射是什么?这些不是我们常规的身份函数!它们必须是从 A 型回到 A 型的态射,这意味着它们是以下形式的修饰函数:
But we are not finished yet. We have defined composition in our new category, but what are the identity morphisms? These are not our regular identity functions! They have to be morphisms from type A back to type A, which means they are embellished functions of the form:
Writer<A> identity(A);Writer<A> identity(A);
它们在组成方面必须表现得像单元。如果你看看我们对组合的定义,你会发现恒等态射应该不加改变地传递它的参数,并且只向日志贡献一个空字符串:
They have to behave like units with respect to composition. If you look at our definition of composition, you’ll see that an identity morphism should pass its argument without change, and only contribute an empty string to the log:
template<class A>
Writer<A> identity(A x) {
return make_pair(x, "");
}template<class A>
Writer<A> identity(A x) {
return make_pair(x, "");
}
您可以轻松地说服自己,我们刚刚定义的范畴确实是合法的范畴。特别是,我们的组合是简单关联的。如果您关注每对的第一个组件所发生的情况,您会发现它只是一个常规函数组合,具有结合性。第二个组件是串联的,并且串联也是关联的。
You can easily convince yourself that the category we have just defined is indeed a legitimate category. In particular, our composition is trivially associative. If you follow what’s happening with the first component of each pair, it’s just a regular function composition, which is associative. The second components are being concatenated, and concatenation is also associative.
精明的读者可能会注意到,很容易将此构造推广到任何幺半群,而不仅仅是字符串幺半群。我们将使用mappendinsidecompose和memptyinside identity(代替+and "")。确实没有理由限制我们只记录字符串。一个好的库编写者应该能够识别使库工作的最低限度的约束——这里日志库的唯一要求是日志具有幺半群属性。
An astute reader may notice that it would be easy to generalize this construction to any monoid, not just the string monoid. We would use mappend inside compose and mempty inside identity (in place of + and ""). There really is no reason to limit ourselves to logging just strings. A good library writer should be able to identify the bare minimum of constraints that make the library work — here the logging library’s only requirement is that the log have monoidal properties.
Haskell 中的相同内容更加简洁,而且我们还从编译器获得了更多帮助。让我们从定义Writer类型开始:
The same thing in Haskell is a little more terse, and we also get a lot more help from the compiler. Let’s start by defining the Writer type:
type Writer a = (a, String)type Writer a = (a, String)
这里我只是定义一个类型别名,相当于 C++ 中的 a typedef(或using)。该类型Writer由类型变量参数化a,相当于一对aand String。对的语法很简单:只有括号中的两个项目,并用逗号分隔。
Here I’m just defining a type alias, an equivalent of a typedef (or using) in C++. The type Writer is parameterized by a type variable a and is equivalent to a pair of a and String. The syntax for pairs is minimal: just two items in parentheses, separated by a comma.
我们的态射是从任意类型到某种Writer类型的函数:
Our morphisms are functions from an arbitrary type to some Writer type:
a -> Writer ba -> Writer b
我们将把组合声明为一个有趣的中缀运算符,有时称为“鱼”:
We’ll declare the composition as a funny infix operator, sometimes called the “fish”:
(>=>) :: (a -> Writer b) -> (b -> Writer c) -> (a -> Writer c)(>=>) :: (a -> Writer b) -> (b -> Writer c) -> (a -> Writer c)
它是一个有两个参数的函数,每个参数都是一个独立的函数,并返回一个函数。第一个参数的类型为(a->Writer b),第二个参数的类型为(b->Writer c),结果为(a->Writer c)。
It’s a function of two arguments, each being a function on its own, and returning a function. The first argument is of the type (a->Writer b), the second is (b->Writer c), and the result is (a->Writer c).
这是这个中缀运算符的定义 - 两个参数m1出现m2在可疑符号的两侧:
Here’s the definition of this infix operator — the two arguments m1 and m2 appearing on either side of the fishy symbol:
m1 >=> m2 = \x ->
let (y, s1) = m1 x
(z, s2) = m2 y
in (z, s1 ++ s2)m1 >=> m2 = \x ->
let (y, s1) = m1 x
(z, s2) = m2 y
in (z, s1 ++ s2)
结果是一个只有一个参数的 lambda 函数x。lambda 写成反斜杠——可以将其视为带有截肢的希腊字母 λ。
The result is a lambda function of one argument x. The lambda is written as a backslash — think of it as the Greek letter λ with an amputated leg.
该let表达式允许您声明辅助变量。这里调用的结果m1与一对变量进行模式匹配(y, s1);m2并且使用第一个模式中的参数调用 的结果y与 相匹配(z, s2)。
The let expression lets you declare auxiliary variables. Here the result of the call to m1 is pattern matched to a pair of variables (y, s1); and the result of the call to m2, with the argument y from the first pattern, is matched to (z, s2).
在 Haskell 中,通常采用模式匹配对,而不是像我们在 C++ 中那样使用访问器。除此之外,两个实现之间有非常简单的对应关系。
It is common in Haskell to pattern match pairs, rather than use accessors, as we did in C++. Other than that there is a pretty straightforward correspondence between the two implementations.
表达式的整体值let在其子句中指定in:这里它是一个对,其第一个组件是z,第二个组件是两个字符串的串联s1++s2。
The overall value of the let expression is specified in its in clause: here it’s a pair whose first component is z and the second component is the concatenation of two strings, s1++s2.
我还将为我们的范畴定义恒等态射,但出于稍后将变得清楚的原因,我将其称为return。
I will also define the identity morphism for our category, but for reasons that will become clear much later, I will call it return.
return :: a -> Writer a
return x = (x, "")return :: a -> Writer a
return x = (x, "")
为了完整起见,让我们使用 Haskell 版本的修饰函数upCase和toWords:
For completeness, let’s have the Haskell versions of the embellished functions upCase and toWords:
upCase :: String -> Writer String
upCase s = (map toUpper s, "upCase ")upCase :: String -> Writer String
upCase s = (map toUpper s, "upCase ")
toWords :: String -> Writer [String]
toWords s = (words s, "toWords ")toWords :: String -> Writer [String]
toWords s = (words s, "toWords ")
该函数map对应于C++ transform. 它将字符函数应用于toUpper字符串s。辅助函数words在标准 Prelude 库中定义。
The function map corresponds to the C++ transform. It applies the character function toUpper to the string s. The auxiliary function words is defined in the standard Prelude library.
最后,两个函数的组合是在fish操作符的帮助下完成的:
Finally, the composition of the two functions is accomplished with the help of the fish operator:
process :: String -> Writer [String]
process = upCase >=> toWordsprocess :: String -> Writer [String]
process = upCase >=> toWords
你可能已经猜到我并没有当场发明这个范畴。这是所谓的 Kleisli 范畴(基于单子的范畴)的一个示例。我们还没有准备好讨论 monad,但我想让您体验一下它们的功能。出于我们有限的目的,Kleisli 范畴具有作为对象的底层编程语言的类型。从类型 A 到类型 B 的态射是使用特定修饰从 A 到派生自 B 的类型的函数。每个 Kleisli 范畴都定义了自己的构成此类态射的方式,以及与该构成相关的恒等态射。(稍后我们将看到不精确的术语“修饰”对应于范畴中的内函子的概念。)
You might have guessed that I haven’t invented this category on the spot. It’s an example of the so called Kleisli category — a category based on a monad. We are not ready to discuss monads yet, but I wanted to give you a taste of what they can do. For our limited purposes, a Kleisli category has, as objects, the types of the underlying programming language. Morphisms from type A to type B are functions that go from A to a type derived from B using the particular embellishment. Each Kleisli category defines its own way of composing such morphisms, as well as the identity morphisms with respect to that composition. (Later we’ll see that the imprecise term “embellishment” corresponds to the notion of an endofunctor in a category.)
我在这篇文章中用作范畴基础的特定 monad 称为writer monad,它用于记录或跟踪函数的执行。它也是在纯计算中嵌入效应的更通用机制的示例。您之前已经看到,我们可以在集合范畴中对编程语言类型和函数进行建模(像往常一样,忽略底部)。在这里,我们将此模型扩展到一个稍微不同的范畴,在该范畴中态射由修饰函数表示,并且它们的组合不仅仅是将一个函数的输出传递到另一个函数的输入。我们还有一个更大的自由度可以玩:构图本身。事实证明,正是这种自由度使得为命令式语言中传统上使用副作用实现的程序提供简单的指称语义成为可能。
The particular monad that I used as the basis of the category in this post is called the writer monad and it’s used for logging or tracing the execution of functions. It’s also an example of a more general mechanism for embedding effects in pure computations. You’ve seen previously that we could model programming-language types and functions in the category of sets (disregarding bottoms, as usual). Here we have extended this model to a slightly different category, a category where morphisms are represented by embellished functions, and their composition does more than just pass the output of one function to the input of another. We have one more degree of freedom to play with: the composition itself. It turns out that this is exactly the degree of freedom which makes it possible to give simple denotational semantics to programs that in imperative languages are traditionally implemented using side effects.
未定义其参数的所有可能值的函数称为偏函数。它实际上并不是数学意义上的函数,因此它不符合标准的分类模型。但是,它可以由返回修饰类型的函数表示optional:
A function that is not defined for all possible values of its argument is called a partial function. It’s not really a function in the mathematical sense, so it doesn’t fit the standard categorical mold. It can, however, be represented by a function that returns an embellished type optional:
template<class A> class optional {
bool _isValid;
A _value;
public:
optional() : _isValid(false) {}
optional(A v) : _isValid(true), _value(v) {}
bool isValid() const { return _isValid; }
A value() const { return _value; }
};template<class A> class optional {
bool _isValid;
A _value;
public:
optional() : _isValid(false) {}
optional(A v) : _isValid(true), _value(v) {}
bool isValid() const { return _isValid; }
A value() const { return _value; }
};
作为示例,以下是修饰函数的实现safe_root:
As an example, here’s the implementation of the embellished function safe_root:
optional<double> safe_root(double x) {
if (x >= 0) return optional<double>{sqrt(x)};
else return optional<double>{};
}optional<double> safe_root(double x) {
if (x >= 0) return optional<double>{sqrt(x)};
else return optional<double>{};
}
这是挑战:
Here’s the challenge:
safe_reciprocal,返回其参数的有效倒数(如果它不为零)。safe_reciprocal that returns a valid reciprocal of its argument, if it’s different from zero.safe_root并safe_reciprocal实现safe_root_reciprocal计算。sqrt(1/x)safe_root and safe_reciprocal to implement safe_root_reciprocal that calculates sqrt(1/x) whenever possible.我感谢 Eric Niebler 阅读了该草案并提供了compose使用 C++14 的高级功能来驱动类型推断的巧妙实现。我能够剪切旧式模板魔法的整个部分,使用类型特征做同样的事情。甩掉包袱!我还感谢 Gershom Bazerman 提供的有用评论帮助我澄清了一些重要观点。
I’m grateful to Eric Niebler for reading the draft and providing the clever implementation of compose that uses advanced features of C++14 to drive type inference. I was able to cut the whole section of old fashioned template magic that did the same thing using type traits. Good riddance! I’m also grateful to Gershom Bazerman for useful comments that helped me clarify some important points.
古希腊剧作家欧里庇得斯曾经说过:“每个人都像是他习惯结交的同伴。” 我们是由我们的关系定义的。没有什么比范畴论更能体现这一点了。如果我们想在一个范畴中挑选出一个特定的对象,我们只能通过描述它与其他对象(及其本身)的关系模式来做到这一点。这些关系由态射定义。
The Ancient Greek playwright Euripides once said: “Every man is like the company he is wont to keep.” We are defined by our relationships. Nowhere is this more true than in category theory. If we want to single out a particular object in a category, we can only do this by describing its pattern of relationships with other objects (and itself). These relationships are defined by morphisms.
范畴论中有一个常见的构造,称为普遍构造,用于根据对象的关系来定义对象。做到这一点的一种方法是选择一种模式,一种由对象和态射构造的特定形状,并在范畴中查找它的所有出现情况。如果它是一个足够常见的模式,并且范畴很大,那么您很可能会获得很多很多的点击率。诀窍是在这些点击中建立某种排名,并选择最适合的。
There is a common construction in category theory called the universal construction for defining objects in terms of their relationships. One way of doing this is to pick a pattern, a particular shape constructed from objects and morphisms, and look for all its occurrences in the category. If it’s a common enough pattern, and the category is large, chances are you’ll have lots and lots of hits. The trick is to establish some kind of ranking among those hits, and pick what could be considered the best fit.
这个过程让人想起我们进行网络搜索的方式。查询就像一个模式。一个非常笼统的查询会给你很大的回忆:很多点击。有些可能相关,有些则不相关。为了消除不相关的命中,您可以优化查询。这提高了它的精度。最后,搜索引擎将对命中进行排名,希望您感兴趣的结果将出现在列表的顶部。
This process is reminiscent of the way we do web searches. A query is like a pattern. A very general query will give you large recall: lots of hits. Some may be relevant, others not. To eliminate irrelevant hits, you refine your query. That increases its precision. Finally, the search engine will rank the hits and, hopefully, the one result that you’re interested in will be at the top of the list.
最简单的形状是单个物体。显然,该形状的实例与给定范畴中的对象一样多。有很多选择。我们需要建立某种排名并尝试找到位于该层次结构顶部的对象。我们唯一可以使用的方法是态射。如果您将态射视为箭头,那么可能存在从范畴的一端到另一端的总体净箭头流。这对于有序范畴(例如部分订单)来说是正确的。我们可以概括对象优先级的概念,如果存在从a到b的箭头(态射),则对象a比对象b “更初始” 。然后,我们将初始对象定义为具有指向所有其他对象的箭头的对象。显然不能保证这样的对象存在,但这没关系。更大的问题是这样的对象可能太多:召回率很好,但精确度不够。解决方案是从有序范畴中获取提示 - 它们最多允许任何两个对象之间有一个箭头:只有一种方法小于或等于另一个对象。这引导我们得到初始对象的定义:
The simplest shape is a single object. Obviously, there are as many instances of this shape as there are objects in a given category. That’s a lot to choose from. We need to establish some kind of ranking and try to find the object that tops this hierarchy. The only means at our disposal are morphisms. If you think of morphisms as arrows, then it’s possible that there is an overall net flow of arrows from one end of the category to another. This is true in ordered categories, for instance in partial orders. We could generalize that notion of object precedence by saying that object a is “more initial” than object b if there is an arrow (a morphism) going from a to b. We would then define the initial object as one that has arrows going to all other objects. Obviously there is no guarantee that such an object exists, and that’s okay. A bigger problem is that there may be too many such objects: The recall is good, but precision is lacking. The solution is to take a hint from ordered categories — they allow at most one arrow between any two objects: there is only one way of being less-than or equal-to another object. Which leads us to this definition of the initial object:
初始对象是对该范畴中的任何对象具有且仅有一个态射的对象。
The initial object is the object that has one and only one morphism going to any object in the category.
然而,即使这样也不能保证初始对象(如果存在)的唯一性。但它保证了次佳的结果:直到同构的唯一性。同构在范畴论中非常重要,所以我很快就会讨论它们。现在,我们只是同意同构的唯一性证明了在初始对象的定义中使用“the”是合理的。
However, even that doesn’t guarantee the uniqueness of the initial object (if one exists). But it guarantees the next best thing: uniqueness up to isomorphism. Isomorphisms are very important in category theory, so I’ll talk about them shortly. For now, let’s just agree that uniqueness up to isomorphism justifies the use of “the” in the definition of the initial object.
以下是一些示例: 部分有序集合(通常称为偏序集)中的初始对象是其最小元素。有些偏序集没有初始对象——比如所有整数的集合,正数和负数,态射具有小于或等于关系。
Here are some examples: The initial object in a partially ordered set (often called a poset) is its least element. Some posets don’t have an initial object — like the set of all integers, positive and negative, with less-than-or-equal relation for morphisms.
在集合和函数范畴中,初始对象是空集。请记住,空集对应于 Haskell 类型Void(C++ 中没有对应的类型),并且Void调用从到任何其他类型的唯一多态函数absurd:
In the category of sets and functions, the initial object is the empty set. Remember, an empty set corresponds to the Haskell type Void (there is no corresponding type in C++) and the unique polymorphic function from Void to any other type is called absurd:
absurd :: Void -> aabsurd :: Void -> a
正是这个态射族构成了Void类型范畴中的初始对象。
It’s this family of morphisms that makes Void the initial object in the category of types.
让我们继续使用单对象模式,但让我们改变对对象进行排序的方式。如果存在从b到a的态射(注意方向的反转),我们会说对象a比对象 b “更终结” 。我们将寻找一个比该范畴中任何其他对象更终端的对象。再次强调,我们将坚持独特性:
Let’s continue with the single-object pattern, but let’s change the way we rank the objects. We’ll say that object a is “more terminal” than object b if there is a morphism going from b to a (notice the reversal of direction). We’ll be looking for an object that’s more terminal than any other object in the category. Again, we will insist on uniqueness:
终端对象是从该范畴中的任何对象具有且仅有一个态射的对象。
The terminal object is the object with one and only one morphism coming to it from any object in the category.
再说一次,终端对象是唯一的,直至同构,我将很快展示这一点。但首先让我们看一些例子。在偏序集中,终端对象(如果存在)是最大的对象。在集合范畴中,终端对象是单例。我们已经讨论过单例——它们对应于voidC++ 中的类型和 Haskell 中的单元类型()。它是一种只有一个值的类型——在 C++ 中是隐式的,在 Haskell 中是显式的,用 表示()。我们还确定从任何类型到单位类型都有一个且仅有一个纯函数:
And again, the terminal object is unique, up to isomorphism, which I will show shortly. But first let’s look at some examples. In a poset, the terminal object, if it exists, is the biggest object. In the category of sets, the terminal object is a singleton. We’ve already talked about singletons — they correspond to the void type in C++ and the unit type () in Haskell. It’s a type that has only one value — implicit in C++ and explicit in Haskell, denoted by (). We’ve also established that there is one and only one pure function from any type to the unit type:
unit :: a -> ()
unit _ = ()unit :: a -> ()
unit _ = ()
因此终端对象的所有条件都得到满足。
so all the conditions for the terminal object are satisfied.
请注意,在此示例中,唯一性条件至关重要,因为还有其他集合(实际上,所有集合,除了空集合)都具有来自每个集合的传入态射。例如,为每种类型定义了一个布尔值函数(谓词):
Notice that in this example the uniqueness condition is crucial, because there are other sets (actually, all of them, except for the empty set) that have incoming morphisms from every set. For instance, there is a Boolean-valued function (a predicate) defined for every type:
yes :: a -> Bool
yes _ = Trueyes :: a -> Bool
yes _ = True
但Bool不是终端对象。Bool每种类型至少有一个多值函数:
But Bool is not a terminal object. There is at least one more Bool-valued function from every type:
no :: a -> Bool
no _ = Falseno :: a -> Bool
no _ = False
坚持唯一性为我们提供了正确的精度,将终端对象的定义缩小为一种类型。
Insisting on uniqueness gives us just the right precision to narrow down the definition of the terminal object to just one type.
您会情不自禁地注意到我们定义初始对象和最终对象的方式之间的对称性。两者之间唯一的区别是态射的方向。事实证明,对于任何范畴 C,我们只需颠倒所有箭头即可定义相反的范畴C op 。只要我们同时重新定义组合,相反的范畴就会自动满足一个范畴的所有要求。如果原始态射f::a->b和g::b->c组成为h::a->cwith h=g∘f,则逆态射fop::b->a和gop::c->b组成为hop::c->awith hop=fop∘gop。反转身份箭头是(双关语警报!)无操作。
You can’t help but to notice the symmetry between the way we defined the initial object and the terminal object. The only difference between the two was the direction of morphisms. It turns out that for any category C we can define the opposite category Cop just by reversing all the arrows. The opposite category automatically satisfies all the requirements of a category, as long as we simultaneously redefine composition. If original morphisms f::a->b and g::b->c composed to h::a->c with h=g∘f, then the reversed morphisms fop::b->a and gop::c->b will compose to hop::c->a with hop=fop∘gop. And reversing the identity arrows is a (pun alert!) no-op.
对偶性是范畴的一个非常重要的属性,因为它使每个研究范畴论的数学家的生产力加倍。对于你提出的每一个结构,都有它的反面;对于你证明的每一个定理,你都可以免费获得一个。相反范畴中的结构通常以“co”为前缀,因此有乘积和余积、单子和共单子、锥体和可锥体、极限和余极限等等。但没有 cocomonad,因为反转箭头两次可以让我们回到原始状态。
Duality is a very important property of categories because it doubles the productivity of every mathematician working in category theory. For every construction you come up with, there is its opposite; and for every theorem you prove, you get one for free. The constructions in the opposite category are often prefixed with “co”, so you have products and coproducts, monads and comonads, cones and cocones, limits and colimits, and so on. There are no cocomonads though, because reversing the arrows twice gets us back to the original state.
由此可见,终端对象是相反范畴中的初始对象。
It follows then that a terminal object is the initial object in the opposite category.
作为程序员,我们很清楚定义相等性是一项艰巨的任务。两个对象相等意味着什么?它们是否必须占用内存中的相同位置(指针相等)?或者它们所有组成部分的值相等就足够了吗?如果一个复数表示为实部和虚部,另一个表示为模数和角度,两个复数是否相等?你可能认为数学家会弄清楚平等的含义,但他们没有。他们有同样的问题,即平等的多个相互竞争的定义。同伦型论有命题平等、内涵平等、外延平等、路径平等。还有较弱的同构概念,甚至较弱的等价概念。
As programmers, we are well aware that defining equality is a nontrivial task. What does it mean for two objects to be equal? Do they have to occupy the same location in memory (pointer equality)? Or is it enough that the values of all their components are equal? Are two complex numbers equal if one is expressed as the real and imaginary part, and the other as modulus and angle? You’d think that mathematicians would have figured out the meaning of equality, but they haven’t. They have the same problem of multiple competing definitions for equality. There is the propositional equality, intensional equality, extensional equality, and equality as a path in homotopy type theory. And then there are the weaker notions of isomorphism, and even weaker of equivalence.
直觉是同构对象看起来是一样的——它们具有相同的形状。这意味着一个对象的每个部分都以一对一的映射方式对应于另一个对象的某些部分。据我们的仪器判断,这两个物体是彼此的完美复制品。从数学上讲,这意味着存在从对象a到对象b 的映射,并且存在从对象b回到对象a的映射,并且它们是彼此的逆。在范畴论中,我们用态射代替映射。同构是可逆的态射;或一对态射,其中一个是另一个的逆。
The intuition is that isomorphic objects look the same — they have the same shape. It means that every part of one object corresponds to some part of another object in a one-to-one mapping. As far as our instruments can tell, the two objects are a perfect copy of each other. Mathematically it means that there is a mapping from object a to object b, and there is a mapping from object b back to object a, and they are the inverse of each other. In category theory we replace mappings with morphisms. An isomorphism is an invertible morphism; or a pair of morphisms, one being the inverse of the other.
我们从复合和恒等的角度来理解逆:态射g是态射f的逆,如果它们的复合是恒等态射。这实际上是两个方程,因为有两种方法可以构成两个态射:
We understand the inverse in terms of composition and identity: Morphism g is the inverse of morphism f if their composition is the identity morphism. These are actually two equations because there are two ways of composing two morphisms:
f . g = id
g . f = idf . g = id
g . f = id
当我说初始(终端)对象在同构上是唯一的时,我的意思是任何两个初始(终端)对象都是同构的。这其实很容易看出。假设我们有两个初始对象 i 1和 i 2。由于 i 1是初始的,因此从 i 1到 i 2存在唯一的态射f。同样,由于 i 2是初始的,因此从 i 2到 i 1存在唯一的态射g。这两个态射的组成是什么?
When I said that the initial (terminal) object was unique up to isomorphism, I meant that any two initial (terminal) objects are isomorphic. That’s actually easy to see. Let’s suppose that we have two initial objects i1 and i2. Since i1 is initial, there is a unique morphism f from i1 to i2. By the same token, since i2 is initial, there is a unique morphism g from i2 to i1. What’s the composition of these two morphisms?
该图中的所有态射都是唯一的
All morphisms in this diagram are unique
组合g∘f必须是从 i 1到 i 1的态射。但 i 1是初始的,因此从 i 1到 i 1只能有一个态射。因为我们在一个范畴中,所以我们知道从 i 1到 i 1存在恒等态射,并且由于只有一个空间,所以一定是它。因此g∘f等于恒等式。类似地,f∘g必须等于恒等,因为从 i 2回到 i 2只能有一个态射。这证明f和g必定互为倒数。因此任何两个初始对象都是同构的。
The composition g∘f must be a morphism from i1 to i1. But i1 is initial so there can only be one morphism going from i1 to i1. Since we are in a category, we know that there is an identity morphism from i1 to i1, and since there is room for only one, that must be it. Therefore g∘f is equal to identity. Similarly, f∘g must be equal to identity, because there can be only one morphism from i2 back to i2. This proves that f and g must be the inverse of each other. Therefore any two initial objects are isomorphic.
请注意,在这个证明中,我们使用了从初始对象到自身的态射的唯一性。如果没有它,我们就无法证明“同构”部分。但为什么我们需要f和g的唯一性?因为初始对象不仅在同构上是唯一的,而且在唯一同构上也是唯一的。原则上,两个对象之间可能存在多个同构,但这里的情况并非如此。这种“唯一性到唯一同构”是所有普遍构造的重要属性。
Notice that in this proof we used the uniqueness of the morphism from the initial object to itself. Without that we couldn’t prove the “up to isomorphism” part. But why do we need the uniqueness of f and g? Because not only is the initial object unique up to isomorphism, it is unique up to unique isomorphism. In principle, there could be more than one isomorphism between two objects, but that’s not the case here. This “uniqueness up to unique isomorphism” is the important property of all universal constructions.
下一个通用结构是产品的结构。我们知道两个集合的笛卡尔积是什么:它是一组对。但是,连接产品集与其组成集的模式是什么?如果我们能弄清楚这一点,我们就能将其推广到其他范畴。
The next universal construction is that of a product. We know what a cartesian product of two sets is: it’s a set of pairs. But what’s the pattern that connects the product set with its constituent sets? If we can figure that out, we’ll be able to generalize it to other categories.
我们只能说,有两个函数,即从产品到每个成分的投影。在 Haskell 中,这两个函数被称为fstand snd,它们分别选择一对的第一个和第二个组件:
All we can say is that there are two functions, the projections, from the product to each of the constituents. In Haskell, these two functions are called fst and snd and they pick, respectively, the first and the second component of a pair:
fst :: (a, b) -> a
fst (x, y) = xfst :: (a, b) -> a
fst (x, y) = x
snd :: (a, b) -> b
snd (x, y) = ysnd :: (a, b) -> b
snd (x, y) = y
在这里,函数是通过模式匹配其参数来定义的:匹配任何对的模式是(x, y),并将其组件提取到变量x和中y。
Here, the functions are defined by pattern matching their arguments: the pattern that matches any pair is (x, y), and it extracts its components into variables x and y.
使用通配符可以进一步简化这些定义:
These definitions can be simplified even further with the use of wildcards:
fst (x, _) = x
snd (_, y) = yfst (x, _) = x
snd (_, y) = y
在C++中,我们会使用模板函数,例如:
In C++, we would use template functions, for instance:
template<class A, class B>
A fst(pair<A, B> const & p) {
return p.first;
}template<class A, class B>
A fst(pair<A, B> const & p) {
return p.first;
}
有了这些看似非常有限的知识,让我们尝试在集合范畴中定义对象和态射的模式,这将引导我们构建两个集合 a 和b的乘积。该模式由一个对象c和两个态射p和q组成,分别将其连接到a和b :
Equipped with this seemingly very limited knowledge, let’s try to define a pattern of objects and morphisms in the category of sets that will lead us to the construction of a product of two sets, a and b. This pattern consists of an object c and two morphisms p and q connecting it to a and b, respectively:
p :: c -> a
q :: c -> bp :: c -> a
q :: c -> b
所有符合此模式的c都将被视为该产品的候选者。可能有很多。
All cs that fit this pattern will be considered candidates for the product. There may be lots of them.
例如,让我们选择两种 Haskell 类型Int和作为我们的成分Bool,并获取其产品的候选样本。
For instance, let’s pick, as our constituents, two Haskell types, Int and Bool, and get a sampling of candidates for their product.
这是一个:Int。可以被视为和Int的乘积的候选者吗?是的,它可以——以下是它的预测:IntBool
Here’s one: Int. Can Int be considered a candidate for the product of Int and Bool? Yes, it can — and here are its projections:
p :: Int -> Int
p x = x
q :: Int -> Bool
q _ = Truep :: Int -> Int
p x = x
q :: Int -> Bool
q _ = True
这是相当蹩脚的,但它符合标准。
That’s pretty lame, but it matches the criteria.
这是另一个:(Int, Int, Bool)。它是三个元素的元组,或三元组。以下两个态射使其成为合法的候选者(我们在三元组上使用模式匹配):
Here’s another one: (Int, Int, Bool). It’s a tuple of three elements, or a triple. Here are two morphisms that make it a legitimate candidate (we are using pattern matching on triples):
p :: (Int, Int, Bool) -> Int
p (x, _, _) = x
q :: (Int, Int, Bool) -> Bool
q (_, _, b) = bp :: (Int, Int, Bool) -> Int
p (x, _, _) = x
q :: (Int, Int, Bool) -> Bool
q (_, _, b) = b
您可能已经注意到,虽然我们的第一个候选太小了,但它只覆盖了Int产品的尺寸;第二个太大了——它虚假地复制了Int维度。
You may have noticed that while our first candidate was too small — it only covered the Int dimension of the product; the second was too big — it spuriously duplicated the Int dimension.
但我们还没有探索通用结构的另一部分:排名。我们希望能够比较模式的两个实例。我们想要将一个候选对象c及其两个投影p和q与另一个候选对象c'及其两个投影p'和q'进行比较。我们想说,如果从c'到c存在态射m ,则c比c' “更好” ——但这太弱了。我们还希望它的预测比c'的预测“更好”或“更普遍” 。这意味着投影p'和q'可以使用m从p和q重建:
But we haven’t explored yet the other part of the universal construction: the ranking. We want to be able to compare two instances of our pattern. We want to compare one candidate object c and its two projections p and q with another candidate object c’ and its two projections p’ and q’. We would like to say that c is “better” than c’ if there is a morphism m from c’ to c — but that’s too weak. We also want its projections to be “better,” or “more universal,” than the projections of c’. What it means is that the projections p’ and q’ can be reconstructed from p and q using m:
p’ = p . m
q’ = q . mp’ = p . m
q’ = q . m
查看这些方程的另一种方式是m将 p'和q'因式分解 。假设这些方程是自然数,并且点是乘法:m是p'和q'共享的公因子。
Another way of looking at these equation is that m factorizes p’ and q’. Just pretend that these equations are in natural numbers, and the dot is multiplication: m is a common factor shared by p’ and q’.
(Int, Bool)为了建立一些直觉,让我向您展示具有两个规范投影的对,fst并且snd确实比我之前提出的两个候选更好。
Just to build some intuitions, let me show you that the pair (Int, Bool) with the two canonical projections, fst and snd is indeed better than the two candidates I presented before.
m第一个候选者的映射是:
The mapping m for the first candidate is:
m :: Int -> (Int, Bool)
m x = (x, True)m :: Int -> (Int, Bool)
m x = (x, True)
事实上,这两个投影p和q可以重构为:
Indeed, the two projections, p and q can be reconstructed as:
p x = fst (m x) = x
q x = snd (m x) = Truep x = fst (m x) = x
q x = snd (m x) = True
第二个例子的m也同样是唯一确定的:
The m for the second example is similarly uniquely determined:
m (x, _, b) = (x, b)m (x, _, b) = (x, b)
我们能够证明这(Int, Bool)比两位候选人中的任何一位都更好。让我们看看为什么相反的情况不成立。我们能找到一些m'可以帮助我们从fst和重建 和的东西吗?sndpq
We were able to show that (Int, Bool) is better than either of the two candidates. Let’s see why the opposite is not true. Could we find some m' that would help us reconstruct fst and snd from p and q?
fst = p . m’
snd = q . m’fst = p . m’
snd = q . m’
在我们的第一个示例中,q总是返回True,并且我们知道有一些对的第二个分量是False。我们无法snd从重建q。
In our first example, q always returned True and we know that there are pairs whose second component is False. We can’t reconstruct snd from q.
第二个示例有所不同:在运行 或 后我们保留了足够的信息p,但是分解和 的q方法不止一种。因为 和都忽略了三元组的第二个组成部分,所以我们可以在其中放入任何内容。我们可以有:fstsndpqm’
The second example is different: we retain enough information after running either p or q, but there is more than one way to factorize fst and snd. Because both p and q ignore the second component of the triple, our m’ can put anything in it. We can have:
m’ (x, b) = (x, x, b)m’ (x, b) = (x, x, b)
或者
or
m’ (x, b) = (x, 42, b)m’ (x, b) = (x, 42, b)
等等。
and so on.
将它们放在一起,给定任何c具有两个投影p和 的类型,笛卡尔积q都有一个独特的mfrom来分解它们。事实上,它只是将和组合成一对。c(a, b)pq
Putting it all together, given any type c with two projections p and q, there is a unique m from c to the cartesian product (a, b) that factorizes them. In fact, it just combines p and q into a pair.
m :: c -> (a, b)
m x = (p x, q x)m :: c -> (a, b)
m x = (p x, q x)
这使得笛卡尔积成为(a, b)我们的最佳匹配,这意味着这种通用构造适用于集合的范畴。它选择任意两个集合的乘积。
That makes the cartesian product (a, b) our best match, which means that this universal construction works in the category of sets. It picks the product of any two sets.
现在让我们忘记集合并使用相同的通用构造来定义任何范畴中的两个对象的乘积。这样的产品并不总是存在,但当它存在时,它是独一无二的,甚至具有独特的同构性。
Now let’s forget about sets and define a product of two objects in any category using the same universal construction. Such product doesn’t always exist, but when it does, it is unique up to a unique isomorphism.
两个对象a和b的乘积是配备有两个投影的对象c,这样对于配备有两个投影的任何其他对象c ',存在从c'到c的唯一态射m来因式分解这些投影。
A product of two objects a and b is the object c equipped with two projections such that for any other object c’ equipped with two projections there is a unique morphism m from c’ to c that factorizes those projections.
从两个候选产生因式分解函数的(高阶)函数m有时称为因式分解器。在我们的例子中,它将是函数:
A (higher order) function that produces the factorizing function m from two candidates is sometimes called the factorizer. In our case, it would be the function:
factorizer :: (c -> a) -> (c -> b) -> (c -> (a, b))
factorizer p q = \x -> (p x, q x)factorizer :: (c -> a) -> (c -> b) -> (c -> (a, b))
factorizer p q = \x -> (p x, q x)
就像范畴论中的每个构造一样,该乘积也有一个对偶,称为余积。当我们反转乘积模式中的箭头时,我们最终得到一个对象c ,该对象 c配备了两个注入,i和j:从a和b到c的态射。
Like every construction in category theory, the product has a dual, which is called the coproduct. When we reverse the arrows in the product pattern, we end up with an object c equipped with two injections, i and j: morphisms from a and b to c.
i :: a -> c
j :: b -> ci :: a -> c
j :: b -> c
排名也是颠倒的:如果存在从c到c ' 的态射m将注入因式分解,则对象c比配备注入i'和j'的对象 c' “更好” :
The ranking is also inverted: object c is “better” than object c’ that is equipped with the injections i’ and j’ if there is a morphism m from c to c’ that factorizes the injections:
i' = m . i
j' = m . ji' = m . i
j' = m . j
“最好的”这样的对象,具有将其连接到任何其他模式的唯一态射,称为余积,并且如果存在,则在唯一同构上是唯一的。
The “best” such object, one with a unique morphism connecting it to any other pattern, is called a coproduct and, if it exists, is unique up to unique isomorphism.
两个对象a和b的余积是配备有两个注入的对象c,这样对于配备有两个注入的任何其他对象c',存在从c到c' 的唯一态射m来因式分解这些注入。
A coproduct of two objects a and b is the object c equipped with two injections such that for any other object c’ equipped with two injections there is a unique morphism m from c to c’ that factorizes those injections.
在集合范畴中,余积是两个集合的不相交并。a和b的不相交并集的元素是a的元素或b的元素。如果两个集合重叠,则不相交的并集包含公共部分的两个副本。您可以将不相交联合的元素视为用指定其来源的标识符进行标记。
In the category of sets, the coproduct is the disjoint union of two sets. An element of the disjoint union of a and b is either an element of a or an element of b. If the two sets overlap, the disjoint union contains two copies of the common part. You can think of an element of a disjoint union as being tagged with an identifier that specifies its origin.
对于程序员来说,从类型角度理解余积更容易:它是两种类型的标记并集。C++ 支持联合,但它们没有标记。这意味着在您的程序中,您必须以某种方式跟踪联盟的哪个成员是有效的。要创建标记联合,您必须定义一个标记(枚举)并将其与联合结合起来。例如,anint和 a的标记联合char const *可以实现为:
For a programmer, it’s easier to understand a coproduct in terms of types: it’s a tagged union of two types. C++ supports unions, but they are not tagged. It means that in your program you have to somehow keep track which member of the union is valid. To create a tagged union, you have to define a tag — an enumeration — and combine it with the union. For instance, a tagged union of an int and a char const * could be implemented as:
struct Contact {
enum { isPhone, isEmail } tag;
union { int phoneNum; char const * emailAddr; };
};struct Contact {
enum { isPhone, isEmail } tag;
union { int phoneNum; char const * emailAddr; };
};
这两个注入可以作为构造函数或函数来实现。例如,这是作为函数的第一次注入PhoneNum:
The two injections can either be implemented as constructors or as functions. For instance, here’s the first injection as a function PhoneNum:
Contact PhoneNum(int n) {
Contact c;
c.tag = isPhone;
c.phoneNum = n;
return c;
}Contact PhoneNum(int n) {
Contact c;
c.tag = isPhone;
c.phoneNum = n;
return c;
}
它将一个整数注入到Contact.
It injects an integer into Contact.
标记联合也称为变体,并且在 boost 库中有一个非常通用的变体实现boost::variant。
A tagged union is also called a variant, and there is a very general implementation of a variant in the boost library, boost::variant.
在 Haskell 中,您可以通过用竖线分隔数据构造函数来将任何数据类型组合到标记联合中。该Contact示例翻译为声明:
In Haskell, you can combine any data types into a tagged union by separating data constructors with a vertical bar. The Contact example translates into the declaration:
data Contact = PhoneNum Int | EmailAddr Stringdata Contact = PhoneNum Int | EmailAddr String
在这里,PhoneNum和EmailAddr既充当构造函数(注入),又充当模式匹配的标签(稍后会详细介绍)。例如,这是使用电话号码构建联系人的方式:
Here, PhoneNum and EmailAddr serve both as constructors (injections), and as tags for pattern matching (more about this later). For instance, this is how you would construct a contact using a phone number:
helpdesk :: Contact;
helpdesk = PhoneNum 2222222helpdesk :: Contact;
helpdesk = PhoneNum 2222222
与 Haskell 中作为原语对内置的乘积的规范实现不同,余积的规范实现是一种名为 的数据类型Either,它在标准 Prelude 中定义为:
Unlike the canonical implementation of the product that is built into Haskell as the primitive pair, the canonical implementation of the coproduct is a data type called Either, which is defined in the standard Prelude as:
Either a b = Left a | Right bEither a b = Left a | Right b
它由两种类型参数化,a并且b有两个构造函数:Left一个采用类型 的值a,另一个Right采用类型 的值b。
It is parameterized by two types, a and b and has two constructors: Left that takes a value of type a, and Right that takes a value of type b.
正如我们为乘积定义了因式分解器一样,我们也可以为余积定义因式分解器。给定一个候选类型c和两个候选注入i和j,因子分解器Either产生因子分解函数:
Just as we’ve defined the factorizer for a product, we can define one for the coproduct. Given a candidate type c and two candidate injections i and j, the factorizer for Either produces the factoring function:
factorizer :: (a -> c) -> (b -> c) -> Either a b -> c
factorizer i j (Left a) = i a
factorizer i j (Right b) = j bfactorizer :: (a -> c) -> (b -> c) -> Either a b -> c
factorizer i j (Left a) = i a
factorizer i j (Right b) = j b
我们已经看到了两组双重定义: 终端对象的定义可以从初始对象的定义中通过反转箭头方向得到;类似地,可以从乘积的定义得到联积的定义。然而,在集合范畴中,初始对象与最终对象有很大不同,余积与乘积也有很大不同。稍后我们将看到乘积的行为类似于乘法,终端对象扮演一的角色;而余积的行为更像是求和,初始对象扮演零的角色。特别是,对于有限集,乘积的大小是各个集合大小的乘积,而余积的大小是大小的总和。
We’ve seen two set of dual definitions: The definition of a terminal object can be obtained from the definition of the initial object by reversing the direction of arrows; in a similar way, the definition of the coproduct can be obtained from that of the product. Yet in the category of sets the initial object is very different from the final object, and coproduct is very different from product. We’ll see later that product behaves like multiplication, with the terminal object playing the role of one; whereas coproduct behaves more like the sum, with the initial object playing the role of zero. In particular, for finite sets, the size of the product is the product of the sizes of individual sets, and the size of the coproduct is the sum of the sizes.
这表明集合的范畴关于箭头的反转不是对称的。
This shows that the category of sets is not symmetric with respect to the inversion of arrows.
请注意,虽然空集对任何集合(函数absurd)都有唯一的态射,但它没有返回态射。单例集合具有来自任何集合的唯一态射,但它也具有每个集合的传出态射(空集合除外)。正如我们之前所看到的,这些来自终端对象的传出态射在选取其他集合的元素方面发挥着非常重要的作用(空集合没有元素,因此没有什么可选取的)。
Notice that while the empty set has a unique morphism to any set (the absurd function), it has no morphisms coming back. The singleton set has a unique morphism coming to it from any set, but it also has outgoing morphisms to every set (except for the empty one). As we’ve seen before, these outgoing morphisms from the terminal object play a very important role of picking elements of other sets (the empty set has no elements, so there’s nothing to pick).
正是单例集与乘积的关系将其与联积区分开来。考虑使用由单位类型表示的单例集(),作为产品模式的另一个(非常差的)候选者。为其配备两个投影p和q: 从单例到每个组成集的函数。每个元素都从任一集合中选择一个具体元素。m因为乘积是通用的,所以从我们的候选者(单例)到乘积也存在(独特的)态射。这种态射从乘积集中选择一个元素——它选择一个具体的对。它还分解了两个投影:
It’s the relationship of the singleton set to the product that sets it apart from the coproduct. Consider using the singleton set, represented by the unit type (), as yet another — vastly inferior — candidate for the product pattern. Equip it with two projections p and q: functions from the singleton to each of the constituent sets. Each selects a concrete element from either set. Because the product is universal, there is also a (unique) morphism m from our candidate, the singleton, to the product. This morphism selects an element from the product set — it selects a concrete pair. It also factorizes the two projections:
p = fst . m
q = snd . mp = fst . m
q = snd . m
当作用于单例值()(单例集的唯一元素)时,这两个方程变为:
When acting on the singleton value (), the only element of the singleton set, these two equations become:
p () = fst (m ())
q () = snd (m ())p () = fst (m ())
q () = snd (m ())
由于m ()是 由 选取的乘积的元素,这些方程告诉用户,从第一组 中m选取的元素是由 选取的对中的第一个分量。类似地,等于第二个分量。这与我们的理解完全一致,即产品的元素是来自组成集合的元素对。pp ()mq ()
Since m () is the element of the product picked by m, these equations tell use that the element picked by p from the first set, p (), is the first component of the pair picked by m. Similarly, q () is equal to the second component. This is in total agreement with our understanding that elements of the product are pairs of elements from the constituent sets.
对余积没有这样简单的解释。我们可以尝试将单例集作为余积的候选者,试图从中提取元素,但是我们将有两次注入进入其中,而不是从中产生两个投影。他们不会告诉我们任何有关其来源的信息(事实上,我们已经看到他们忽略了输入参数)。从余积到我们的单例的独特态射也不会。从初始对象的方向看与从末端看时,集合的范畴看起来非常不同。
There is no such simple interpretation of the coproduct. We could try the singleton set as a candidate for a coproduct, in an attempt to extract the elements from it, but there we would have two injections going into it rather than two projections coming out of it. They’d tell us nothing about their sources (in fact, we’ve seen that they ignore the input parameter). Neither would the unique morphism from the coproduct to our singleton. The category of sets just looks very different when seen from the direction of the initial object than it does when seen from the terminal end.
这不是集合的内在属性,而是函数的属性,我们在Set中将其用作态射。一般来说,函数是不对称的。让我解释。
This is not an intrinsic property of sets, it’s a property of functions, which we use as morphisms in Set. Functions are, in general, asymmetric. Let me explain.
必须为其域集的每个元素定义一个函数(在编程中,我们称其为总函数),但它不必覆盖整个共域。我们已经看到了一些极端的情况:来自单例集合的函数——仅选择共域中的单个元素的函数。(实际上,来自空集的函数才是真正的极值。)当定义域的大小远小于余域的大小时,我们通常将此类函数视为将域嵌入到余域中。例如,我们可以将单例集中的函数视为将其单个元素嵌入到共域中。我将它们称为嵌入函数,但数学家更喜欢给相反的名称起一个名字:紧密填充其共域的函数称为满射或到。
A function must be defined for every element of its domain set (in programming, we call it a total function), but it doesn’t have to cover the whole codomain. We’ve seen some extreme cases of it: functions from a singleton set — functions that select just a single element in the codomain. (Actually, functions from an empty set are the real extremes.) When the size of the domain is much smaller than the size of the codomain, we often think of such functions as embedding the domain in the codomain. For instance, we can think of a function from a singleton set as embedding its single element in the codomain. I call them embedding functions, but mathematicians prefer to give a name to the opposite: functions that tightly fill their codomains are called surjective or onto.
不对称的另一个来源是函数可以将域集的许多元素映射到共域的一个元素。他们可以让它们崩溃。极端情况是将整个集合映射到单个实例的函数。您已经看到了unit执行此操作的多态函数。崩溃只能通过组合来加剧。两个折叠函数的组合比单个函数的折叠程度更高。数学家对非折叠函数有一个名字:他们称它们为单射函数或一对一函数
The other source of asymmetry is that functions are allowed to map many elements of the domain set into one element of the codomain. They can collapse them. The extreme case are functions that map whole sets into a singleton. You’ve seen the polymorphic unit function that does just that. The collapsing can only be compounded by composition. A composition of two collapsing functions is even more collapsing than the individual functions. Mathematicians have a name for non-collapsing functions: they call them injective or one-to-one
当然,有些函数既不嵌入也不折叠。它们被称为双射,并且它们是真正对称的,因为它们是可逆的。在集合范畴中,同构与双射相同。
Of course there are some functions that are neither embedding nor collapsing. They are called bijections and they are truly symmetric, because they are invertible. In the category of sets, an isomorphism is the same as a bijection.
Either用您最喜欢的语言(Haskell 除外)将 Haskell 的等效项实现为泛型类型。Either as a generic type in your favorite language (other than Haskell).证明这是一个比配备两次注射Either“更好”的副产品:int
int i(int n) { return n; }
int j(bool b) { return b? 0: 1; }
提示:定义一个函数
int m(Either const & e);
因式分解i和j。
Show that Either is a “better” coproduct than int equipped with two injections:
int i(int n) { return n; }
int j(bool b) { return b? 0: 1; }
Hint: Define a function
int m(Either const & e);
that factorizes i and j.
int两次注射i和j不能“更好”于Either?int with the two injections i and j cannot be “better” than Either?仍在继续:这些注射怎么样?
int i(int n) {
if (n < 0) return n;
return n + 2;
}
int j(bool b) { return b? 0: 1; }Still continuing: What about these injections?
int i(int n) {
if (n < 0) return n;
return n + 2;
}
int j(bool b) { return b? 0: 1; }int,bool它不能比 更好,Either因为它允许从 到 的多个可接受的态射Either。int and bool that cannot be better than Either because it allows multiple acceptable morphisms from it to Either.我感谢 Gershom Bazerman 在发表之前审阅了这篇文章并激发了讨论。
I’m grateful to Gershom Bazerman for reviewing this post before publication and for stimulating discussions.
我们已经看到了组合类型的两种基本方法:使用乘积和余积。事实证明,日常编程中的许多数据结构都可以使用这两种机制来构建。这一事实具有重要的实际后果。数据结构的许多属性都是可组合的。例如,如果您知道如何比较基本类型的值是否相等,并且知道如何将这些比较推广到乘积和余积类型,则可以自动派生复合类型的相等运算符。在 Haskell 中,您可以针对复合类型的大型子集自动导出相等、比较、与字符串之间的转换等。
We’ve seen two basic ways of combining types: using a product and a coproduct. It turns out that a lot of data structures in everyday programming can be built using just these two mechanisms. This fact has important practical consequences. Many properties of data structures are composable. For instance, if you know how to compare values of basic types for equality, and you know how to generalize these comparisons to product and coproduct types, you can automate the derivation of equality operators for composite types. In Haskell you can automatically derive equality, comparison, conversion to and from string, and more, for a large subset of composite types.
让我们仔细看看编程中出现的乘积和求和类型。
Let’s have a closer look at product and sum types as they appear in programming.
编程语言中两种类型的乘积的规范实现是一对。在 Haskell 中,pair 是原始类型构造函数;在 C++ 中,它是标准库中定义的相对复杂的模板。
The canonical implementation of a product of two types in a programming language is a pair. In Haskell, a pair is a primitive type constructor; in C++ it’s a relatively complex template defined in the Standard Library.
Pair 不严格可交换:pair(Int, Bool)不能替代pair (Bool, Int),即使它们携带相同的信息。然而,它们在同构上是可交换的——同构由函数给出swap(这是它自己的逆):
Pairs are not strictly commutative: a pair (Int, Bool) cannot be substituted for a pair (Bool, Int), even though they carry the same information. They are, however, commutative up to isomorphism — the isomorphism being given by the swap function (which is its own inverse):
swap :: (a, b) -> (b, a)
swap (x, y) = (y, x)swap :: (a, b) -> (b, a)
swap (x, y) = (y, x)
您可以将这两对视为简单地使用不同的格式来存储相同的数据。这就像大端与小端一样。
You can think of the two pairs as simply using a different format for storing the same data. It’s just like big endian vs. little endian.
您可以通过在对内嵌套对来将任意数量的类型组合到产品中,但有一种更简单的方法:嵌套对相当于元组。这是不同方式的嵌套对是同构的结果。如果您想将三种类型按顺序组合在一个产品中, a、b、 和,您可以通过两种方式进行:c
You can combine an arbitrary number of types into a product by nesting pairs inside pairs, but there is an easier way: nested pairs are equivalent to tuples. It’s the consequence of the fact that different ways of nesting pairs are isomorphic. If you want to combine three types in a product, a, b, and c, in this order, you can do it in two ways:
((a, b), c)((a, b), c)
或者
or
(a, (b, c))(a, (b, c))
这些类型是不同的 - 您不能将其中一个传递给需要另一个的函数 - 但它们的元素是一一对应的。有一个函数可以将一个映射到另一个:
These types are different — you can’t pass one to a function that expects the other — but their elements are in one-to-one correspondence. There is a function that maps one to another:
alpha :: ((a, b), c) -> (a, (b, c))
alpha ((x, y), z) = (x, (y, z))alpha :: ((a, b), c) -> (a, (b, c))
alpha ((x, y), z) = (x, (y, z))
并且这个函数是可逆的:
and this function is invertible:
alpha_inv :: (a, (b, c)) -> ((a, b), c)
alpha_inv (x, (y, z)) = ((x, y), z)alpha_inv :: (a, (b, c)) -> ((a, b), c)
alpha_inv (x, (y, z)) = ((x, y), z)
所以这是一个同构。这些只是重新打包相同数据的不同方式。
so it’s an isomorphism. These are just different ways of repackaging the same data.
您可以将产品类型的创建解释为类型上的二元运算。从这个角度来看,上面的同构看起来非常像我们在幺半群中看到的结合律:
You can interpret the creation of a product type as a binary operation on types. From that perspective, the above isomorphism looks very much like the associativity law we’ve seen in monoids:
(a * b) * c = a * (b * c)(a * b) * c = a * (b * c)
不同的是,在幺半群的情况下,两种组合乘积的方式是相等的,而在这里它们只是“同构”相等。
Except that, in the monoid case, the two ways of composing products were equal, whereas here they are only equal “up to isomorphism.”
如果我们可以接受同构,并且不坚持严格相等,我们可以更进一步,证明单位类型()是乘积的单位,就像 1 是乘法单位一样。事实上,某种类型的值与单位的配对a不会添加任何信息。方式:
If we can live with isomorphisms, and don’t insist on strict equality, we can go even further and show that the unit type, (), is the unit of the product the same way 1 is the unit of multiplication. Indeed, the pairing of a value of some type a with a unit doesn’t add any information. The type:
(a, ())(a, ())
同构于a. 这是同构:
is isomorphic to a. Here’s the isomorphism:
rho :: (a, ()) -> a
rho (x, ()) = xrho :: (a, ()) -> a
rho (x, ()) = x
rho_inv :: a -> (a, ())
rho_inv x = (x, ())rho_inv :: a -> (a, ())
rho_inv x = (x, ())
这些观察可以通过说Set(集合的范畴)是一个幺半群范畴来形式化。它是一个范畴,也是一个幺半群,从某种意义上说,您可以将对象相乘(在这里,采用它们的笛卡尔积)。我将更多地讨论幺半群范畴,并在将来给出完整的定义。
These observations can be formalized by saying that Set (the category of sets) is a monoidal category. It’s a category that’s also a monoid, in the sense that you can multiply objects (here, take their cartesian product). I’ll talk more about monoidal categories, and give the full definition in the future.
在 Haskell 中有一种更通用的方法来定义乘积类型——特别是,正如我们很快就会看到的,当它们与求和类型组合时。它使用带有多个参数的命名构造函数。例如,一对可以定义为:
There is a more general way of defining product types in Haskell — especially, as we’ll see soon, when they are combined with sum types. It uses named constructors with multiple arguments. A pair, for instance, can be defined alternatively as:
data Pair a b = P a bdata Pair a b = P a b
这里,Pair a b是由另外两个类型参数化的类型的名称,a并且b;并且P是数据构造函数的名称。您可以通过将两个类型传递给Pair类型构造函数来定义对类型。您可以通过将两个适当类型的值传递给构造函数来构造对值P。例如,让我们将一个值定义stmt为一对String和Bool:
Here, Pair a b is the name of the type paremeterized by two other types, a and b; and P is the name of the data constructor. You define a pair type by passing two types to the Pair type constructor. You construct a pair value by passing two values of appropriate types to the constructor P. For instance, let’s define a value stmt as a pair of String and Bool:
stmt :: Pair String Bool
stmt = P "This statements is" Falsestmt :: Pair String Bool
stmt = P "This statements is" False
第一行是类型声明。它使用类型构造函数Pair,用String和Bool替换 的泛型定义中的a和。第二行通过将具体字符串和具体布尔值传递给数据构造函数来定义实际值。类型构造函数用于构造类型;数据构造函数,构造值。bPairP
The first line is the type declaration. It uses the type constructor Pair, with String and Bool replacing a and the b in the generic definition of Pair. The second line defines the actual value by passing a concrete string and a concrete Boolean to the data constructor P. Type constructors are used to construct types; data constructors, to construct values.
由于类型和数据构造函数的名称空间在 Haskell 中是分开的,因此您经常会看到两者使用相同的名称,如下所示:
Since the name spaces for type and data constructors are separate in Haskell, you will often see the same name used for both, as in:
data Pair a b = Pair a bdata Pair a b = Pair a b
如果您仔细观察,您甚至可能会将内置对类型视为此类声明的变体,其中名称Pair被替换为二元运算符(,)。事实上,您可以(,)像任何其他命名构造函数一样使用并使用前缀表示法创建对:
And if you squint hard enough, you may even view the built-in pair type as a variation on this kind of declaration, where the name Pair is replaced with the binary operator (,). In fact you can use (,) just like any other named constructor and create pairs using prefix notation:
stmt = (,) "This statement is" Falsestmt = (,) "This statement is" False
同样,您可以用来(,,)创建三元组,等等。
Similarly, you can use (,,) to create triples, and so on.
您还可以定义特定的命名产品类型,而不是使用通用对或元组,如下所示:
Instead of using generic pairs or tuples, you can also define specific named product types, as in:
data Stmt = Stmt String Booldata Stmt = Stmt String Bool
String它只是和的乘积Bool,但它有自己的名称和构造函数。这种声明方式的优点是您可以定义许多具有相同内容但不同含义和功能的类型,并且这些类型不能相互替换。
which is just a product of String and Bool, but it’s given its own name and constructor. The advantage of this style of declaration is that you may define many types that have the same content but different meaning and functionality, and which cannot be substituted for each other.
使用元组和多参数构造函数进行编程可能会变得混乱且容易出错——跟踪哪个组件代表什么。通常最好为组件命名。具有命名字段的产品类型在 Haskell 中称为“记录”,struct在 C 中称为“a”。
Programming with tuples and multi-argument constructors can get messy and error prone — keeping track of which component represents what. It’s often preferable to give names to components. A product type with named fields is called a record in Haskell, and a struct in C.
让我们看一个简单的例子。我们想要通过组合两个字符串(名称和符号)来描述化学元素;和一个整数,原子序数;到一个数据结构中。我们可以使用元组(String, String, Int)并记住哪个组件代表什么。我们将通过模式匹配来提取组件,就像在这个函数中检查元素的符号是否是其名称的前缀(例如He是Helium的前缀):
Let’s have a look at a simple example. We want to describe chemical elements by combining two strings, name and symbol; and an integer, the atomic number; into one data structure. We can use a tuple (String, String, Int) and remember which component represents what. We would extract components by pattern matching, as in this function that checks if the symbol of the element is the prefix of its name (as in He being the prefix of Helium):
startsWithSymbol :: (String, String, Int) -> Bool
startsWithSymbol (name, symbol, _) = isPrefixOf symbol namestartsWithSymbol :: (String, String, Int) -> Bool
startsWithSymbol (name, symbol, _) = isPrefixOf symbol name
这段代码很容易出错,并且难以阅读和维护。定义一条记录要好得多:
This code is error prone, and is hard to read and maintain. It’s much better to define a record:
data Element = Element { name :: String
, symbol :: String
, atomicNumber :: Int }data Element = Element { name :: String
, symbol :: String
, atomicNumber :: Int }
这两种表示形式是同构的,正如这两个互为逆的转换函数所证明的那样:
The two representations are isomorphic, as witnessed by these two conversion functions, which are the inverse of each other:
tupleToElem :: (String, String, Int) -> Element
tupleToElem (n, s, a) = Element { name = n
, symbol = s
, atomicNumber = a }tupleToElem :: (String, String, Int) -> Element
tupleToElem (n, s, a) = Element { name = n
, symbol = s
, atomicNumber = a }
elemToTuple :: Element -> (String, String, Int)
elemToTuple e = (name e, symbol e, atomicNumber e)elemToTuple :: Element -> (String, String, Int)
elemToTuple e = (name e, symbol e, atomicNumber e)
请注意,记录字段的名称也可用作访问这些字段的函数。例如,从atomicNumber e检索字段。我们使用以下类型的函数:atomicNumbereatomicNumber
Notice that the names of record fields also serve as functions to access these fields. For instance, atomicNumber e retrieves the atomicNumber field from e. We use atomicNumber as a function of the type:
atomicNumber :: Element -> IntatomicNumber :: Element -> Int
使用 的记录语法Element,我们的函数startsWithSymbol变得更具可读性:
With the record syntax for Element, our function startsWithSymbol becomes more readable:
startsWithSymbol :: Element -> Bool
startsWithSymbol e = isPrefixOf (symbol e) (name e)startsWithSymbol :: Element -> Bool
startsWithSymbol e = isPrefixOf (symbol e) (name e)
我们甚至可以使用 Haskell 技巧,isPrefixOf通过用反引号将函数转换为中缀运算符,并使其读起来几乎像一个句子:
We could even use the Haskell trick of turning the function isPrefixOf into an infix operator by surrounding it with backquotes, and make it read almost like a sentence:
startsWithSymbol e = symbol e `isPrefixOf` name estartsWithSymbol e = symbol e `isPrefixOf` name e
在这种情况下可以省略括号,因为中缀运算符的优先级低于函数调用。
The parentheses could be omitted in this case, because an infix operator has lower precedence than a function call.
正如集合范畴中的乘积产生乘积类型一样,余积产生和类型。Haskell 中求和类型的规范实现是:
Just as the product in the category of sets gives rise to product types, the coproduct gives rise to sum types. The canonical implementation of a sum type in Haskell is:
data Either a b = Left a | Right bdata Either a b = Left a | Right b
与对一样,Eithers 是可交换的(直到同构),可以嵌套,并且嵌套顺序无关(直到同构)。例如,我们可以定义一个三元组的总和:
And like pairs, Eithers are commutative (up to isomorphism), can be nested, and the nesting order is irrelevant (up to isomorphism). So we can, for instance, define a sum equivalent of a triple:
data OneOfThree a b c = Sinistral a | Medial b | Dextral cdata OneOfThree a b c = Sinistral a | Medial b | Dextral c
等等。
and so on.
事实证明,集合也是一个关于余积的(对称)幺半群范畴。二元运算的作用是由不相交和来扮演的,单位元素的角色是由初始对象来扮演的。就类型而言,我们将Either幺半群算子 和Void,即无人居住的类型,作为它的中性元素。您可以将其视为Either加号,并将Void其视为零。事实上,添加Void到 sum 类型不会改变其内容。例如:
It turns out that Set is also a (symmetric) monoidal category with respect to coproduct. The role of the binary operation is played by the disjoint sum, and the role of the unit element is played by the initial object. In terms of types, we have Either as the monoidal operator and Void, the uninhabited type, as its neutral element. You can think of Either as plus, and Void as zero. Indeed, adding Void to a sum type doesn’t change its content. For instance:
Either a VoidEither a Void
同构于a. 那是因为没有办法构造Right这种类型的版本——没有 type 的值Void。的唯一居民Either a Void是使用Left构造函数构造的,它们只是封装类型的值a。因此,象征性地,a + 0 = a.
is isomorphic to a. That’s because there is no way to construct a Right version of this type — there isn’t a value of type Void. The only inhabitants of Either a Void are constructed using the Left constructors and they simply encapsulate a value of type a. So, symbolically, a + 0 = a.
Sum 类型在 Haskell 中非常常见,但它们的 C++ 等效项、联合或变体则不太常见。这有几个原因。
Sum types are pretty common in Haskell, but their C++ equivalents, unions or variants, are much less common. There are several reasons for that.
首先,最简单的求和类型只是枚举,并且是enum在 C++ 中实现的。相当于 Haskell sum 类型:
First of all, the simplest sum types are just enumerations and are implemented using enum in C++. The equivalent of the Haskell sum type:
data Color = Red | Green | Bluedata Color = Red | Green | Blue
是C++:
is the C++:
enum { Red, Green, Blue };enum { Red, Green, Blue };
更简单的求和类型:
An even simpler sum type:
data Bool = True | Falsedata Bool = True | False
是 C++ 中的原语bool。
is the primitive bool in C++.
对值的存在或不存在进行编码的简单求和类型在 C++ 中使用特殊技巧和“不可能”值(如空字符串、负数、空指针等)以不同方式实现。这种可选性(如果有意)在 Haskell 中表达使用Maybe类型:
Simple sum types that encode the presence or absence of a value are variously implemented in C++ using special tricks and “impossible” values, like empty strings, negative numbers, null pointers, etc. This kind of optionality, if deliberate, is expressed in Haskell using the Maybe type:
data Maybe a = Nothing | Just adata Maybe a = Nothing | Just a
该Maybe类型是两种类型的总和。如果将两个构造函数分成单独的类型,您可以看到这一点。第一个看起来像这样:
The Maybe type is a sum of two types. You can see this if you separate the two constructors into individual types. The first one would look like this:
data NothingType = Nothingdata NothingType = Nothing
它是一个具有一个名为 的值的枚举Nothing。换句话说,它是一个单例,相当于单元类型()。第二部分:
It’s an enumeration with one value called Nothing. In other words, it’s a singleton, which is equivalent to the unit type (). The second part:
data JustType a = Just adata JustType a = Just a
只是类型的封装a。我们可以编码Maybe为:
is just an encapsulation of the type a. We could have encoded Maybe as:
data Maybe a = Either () adata Maybe a = Either () a
在 C++ 中,通常使用指针来伪造更复杂的求和类型。指针可以为 null,也可以指向特定类型的值。例如,Haskell 列表类型,可以定义为(递归)求和类型:
More complex sum types are often faked in C++ using pointers. A pointer can be either null, or point to a value of specific type. For instance, a Haskell list type, which can be defined as a (recursive) sum type:
List a = Nil | Cons a (List a)List a = Nil | Cons a (List a)
可以使用空指针技巧转换为 C++ 来实现空列表:
can be translated to C++ using the null pointer trick to implement the empty list:
template<class A>
class List {
Node<A> * _head;
public:
List() : _head(nullptr) {} // Nil
List(A a, List<A> l) // Cons
: _head(new Node<A>(a, l))
{}
};template<class A>
class List {
Node<A> * _head;
public:
List() : _head(nullptr) {} // Nil
List(A a, List<A> l) // Cons
: _head(new Node<A>(a, l))
{}
};
请注意,两个 Haskell 构造函数Nil和Cons被转换为两个List具有类似参数的重载构造函数(无, for Nil;以及值和列表 for Cons)。该类List不需要标记来区分总和类型的两个组件。相反,它使用特殊nullptr值 来_head编码Nil。
Notice that the two Haskell constructors Nil and Cons are translated into two overloaded List constructors with analogous arguments (none, for Nil; and a value and a list for Cons). The List class doesn’t need a tag to distinguish between the two components of the sum type. Instead it uses the special nullptr value for _head to encode Nil.
不过,Haskell 和 C++ 类型之间的主要区别在于 Haskell 数据结构是不可变的。如果使用一个特定的构造函数创建一个对象,该对象将永远记住使用了哪个构造函数以及传递给它的参数。因此,Maybe创建为 的对象Just "energy"永远不会变成Nothing. 同样,空列表将永远为空,并且三个元素的列表将始终具有相同的三个元素。
The main difference, though, between Haskell and C++ types is that Haskell data structures are immutable. If you create an object using one particular constructor, the object will forever remember which constructor was used and what arguments were passed to it. So a Maybe object that was created as Just "energy" will never turn into Nothing. Similarly, an empty list will forever be empty, and a list of three elements will always have the same three elements.
正是这种不变性使得构造可逆。给定一个对象,您始终可以将其拆解成其构造中使用的部件。这种解构是通过模式匹配完成的,并且它重用构造函数作为模式。构造函数参数(如果有)将替换为变量(或其他模式)。
It’s this immutability that makes construction reversible. Given an object, you can always disassemble it down to parts that were used in its construction. This deconstruction is done with pattern matching and it reuses constructors as patterns. Constructor arguments, if any, are replaced with variables (or other patterns).
该List数据类型有两个构造函数,因此任意数据类型的解构List使用与这些构造函数相对应的两种模式。一个匹配空Nil列表,另一个匹配Cons构造列表。例如,下面是 s 上一个简单函数的定义List:
The List data type has two constructors, so the deconstruction of an arbitrary List uses two patterns corresponding to those constructors. One matches the empty Nil list, and the other a Cons-constructed list. For instance, here’s the definition of a simple function on Lists:
maybeTail :: List a -> Maybe (List a)
maybeTail Nil = Nothing
maybeTail (Cons _ t) = Just tmaybeTail :: List a -> Maybe (List a)
maybeTail Nil = Nothing
maybeTail (Cons _ t) = Just t
定义的第一部分maybeTail使用Nil构造函数作为模式并返回Nothing。第二部分使用Cons构造函数作为模式。它用通配符替换第一个构造函数参数,因为我们对它不感兴趣。第二个参数 toCons绑定到变量t(我将这些东西称为变量,尽管严格来说,它们永远不会改变:一旦绑定到表达式,变量就永远不会改变)。返回值为Just t. 现在,根据您的List创建方式,它将匹配其中一个子句。如果它是使用 创建的Cons,则将检索传递给它的两个参数(并丢弃第一个参数)。
The first part of the definition of maybeTail uses the Nil constructor as pattern and returns Nothing. The second part uses the Cons constructor as pattern. It replaces the first constructor argument with a wildcard, because we are not interested in it. The second argument to Cons is bound to the variable t (I will call these things variables even though, strictly speaking, they never vary: once bound to an expression, a variable never changes). The return value is Just t. Now, depending on how your List was created, it will match one of the clauses. If it was created using Cons, the two arguments that were passed to it will be retrieved (and the first discarded).
更复杂的求和类型是使用多态类层次结构在 C++ 中实现的。具有共同祖先的类族可以被理解为一种变体类型,其中vtable充当隐藏标记。在 Haskell 中,通过构造函数上的模式匹配以及调用专门的代码来完成,而在 C++ 中,则通过根据 vtable 指针分派对虚拟函数的调用来完成。
Even more elaborate sum types are implemented in C++ using polymorphic class hierarchies. A family of classes with a common ancestor may be understood as one variant type, in which the vtable serves as a hidden tag. What in Haskell would be done by pattern matching on the constructor, and by calling specialized code, in C++ is accomplished by dispatching a call to a virtual function based on the vtable pointer.
union由于联合中的内容受到严格限制,因此在 C++ 中很少会看到用作求和类型。您甚至不能将 a 放入std::string联合中,因为它有一个复制构造函数。
You will rarely see union used as a sum type in C++ because of severe limitations on what can go into a union. You can’t even put a std::string into a union because it has a copy constructor.
单独来看,乘积和求和类型可用于定义各种有用的数据结构,但真正的优势来自于将两者结合起来。我们再次调用构图的力量。
Taken separately, product and sum types can be used to define a variety of useful data structures, but the real strength comes from combining the two. Once again we are invoking the power of composition.
让我们总结一下到目前为止我们所发现的内容。我们已经看到了类型系统底层的两种可交换幺半群结构:我们将和类型作为Void中性元素,将乘积类型以单位类型()作为中性元素。我们希望将它们视为类似于加法和乘法。在这个类比中,Void对应于零,单位 ,()对应于一。
Let’s summarize what we’ve discovered so far. We’ve seen two commutative monoidal structures underlying the type system: We have the sum types with Void as the neutral element, and the product types with the unit type, (), as the neutral element. We’d like to think of them as analogous to addition and multiplication. In this analogy, Void would correspond to zero, and unit, (), to one.
让我们看看我们可以将这个类比延伸到什么程度。例如,乘以零会得到零吗?换句话说,具有一个组件的产品类型是否与 是Void同构的Void?例如,是否可以创建一对,例如Int和Void?
Let’s see how far we can stretch this analogy. For instance, does multiplication by zero give zero? In other words, is a product type with one component being Void isomorphic to Void? For example, is it possible to create a pair of, say Int and Void?
要创建一对,您需要两个值。尽管您可以轻松得出整数,但没有 类型的值Void。因此,对于任何类型a,该类型(a, Void)都是无人居住的——没有值——因此等价于Void。换句话说,a*0 = 0.
To create a pair you need two values. Although you can easily come up with an integer, there is no value of type Void. Therefore, for any type a, the type (a, Void) is uninhabited — has no values — and is therefore equivalent to Void. In other words, a*0 = 0.
连接加法和乘法的另一件事是分配律:
Another thing that links addition and multiplication is the distributive property:
a * (b + c) = a * b + a * ca * (b + c) = a * b + a * c
它也适用于乘积和总和类型吗?是的,确实如此——像往常一样,直到同构为止。左侧对应于类型:
Does it also hold for product and sum types? Yes, it does — up to isomorphisms, as usual. The left hand side corresponds to the type:
(a, Either b c)(a, Either b c)
右侧对应于以下类型:
and the right hand side corresponds to the type:
Either (a, b) (a, c)Either (a, b) (a, c)
这是以一种方式转换它们的函数:
Here’s the function that converts them one way:
prodToSum :: (a, Either b c) -> Either (a, b) (a, c)
prodToSum (x, e) =
case e of
Left y -> Left (x, y)
Right z -> Right (x, z)prodToSum :: (a, Either b c) -> Either (a, b) (a, c)
prodToSum (x, e) =
case e of
Left y -> Left (x, y)
Right z -> Right (x, z)
这是一个相反的情况:
and here’s one that goes the other way:
sumToProd :: Either (a, b) (a, c) -> (a, Either b c)
sumToProd e =
case e of
Left (x, y) -> (x, Left y)
Right (x, z) -> (x, Right z)sumToProd :: Either (a, b) (a, c) -> (a, Either b c)
sumToProd e =
case e of
Left (x, y) -> (x, Left y)
Right (x, z) -> (x, Right z)
该case of语句用于函数内部的模式匹配。每个模式后面都有一个箭头和模式匹配时要计算的表达式。例如,如果您prodToSum使用以下值进行调用:
The case of statement is used for pattern matching inside functions. Each pattern is followed by an arrow and the expression to be evaluated when the pattern matches. For instance, if you call prodToSum with the value:
prod1 :: (Int, Either String Float)
prod1 = (2, Left "Hi!")prod1 :: (Int, Either String Float)
prod1 = (2, Left "Hi!")
in将等于e。它将匹配模式,替换。由于已与 匹配,因此该子句的结果以及整个函数将如预期的那样为 。case e ofLeft "Hi!"Left y"Hi!"yx2case ofLeft (2, "Hi!")
the e in case e of will be equal to Left "Hi!". It will match the pattern Left y, substituting "Hi!" for y. Since the x has already been matched to 2, the result of the case of clause, and the whole function, will be Left (2, "Hi!"), as expected.
我不会证明这两个函数是互逆的,但如果你仔细想想,它们一定是互逆的!他们只是简单地重新打包两个数据结构的内容。这是相同的数据,只是格式不同。
I’m not going to prove that these two functions are the inverse of each other, but if you think about it, they must be! They are just trivially re-packing the contents of the two data structures. It’s the same data, only different format.
数学家给这样两个交织在一起的幺半群起了一个名字:它被称为半环。它不是一个完整的环,因为我们无法定义类型的减法。这就是为什么半环有时被称为rig ,它是“没有n的环”(负数)的双关语。但除此之外,我们还可以通过将有关形成环的自然数的语句转换为有关类型的语句来获得很多帮助。这是一个翻译表,其中包含一些有趣的条目:
Mathematicians have a name for such two intertwined monoids: it’s called a semiring. It’s not a full ring, because we can’t define subtraction of types. That’s why a semiring is sometimes called a rig, which is a pun on “ring without an n” (negative). But barring that, we can get a lot of mileage from translating statements about, say, natural numbers, which form a ring, to statements about types. Here’s a translation table with some entries of interest:
| 数字 | 类型 |
|---|---|
| 0 | Void |
| 1 | () |
| a+b | Either a b = Left a | Right b |
| 一个 * 乙 | (a, b) 或者 Pair a b = Pair a b |
| 2 = 1 + 1 | data Bool = True | False |
| 1+一个 | data Maybe = Nothing | Just a |
列表类型非常有趣,因为它被定义为方程的解。我们定义的类型出现在等式两边:
The list type is quite interesting, because it’s defined as a solution to an equation. The type we are defining appears on both sides of the equation:
List a = Nil | Cons a (List a)List a = Nil | Cons a (List a)
如果我们进行通常的替换,并用 替换List a,x我们得到等式:
If we do our usual substitutions, and also replace List a with x, we get the equation:
x = 1 + a * xx = 1 + a * x
我们无法使用传统的代数方法来解决它,因为我们无法对类型进行减法或除法。但我们可以尝试一系列替换,其中我们不断将x右侧替换为(1 + a*x), 并使用分配律。这导致了以下系列:
We can’t solve it using traditional algebraic methods because we can’t subtract or divide types. But we can try a series of substitutions, where we keep replacing x on the right hand side with (1 + a*x), and use the distributive property. This leads to the following series:
x = 1 + a*x
x = 1 + a*(1 + a*x) = 1 + a + a*a*x
x = 1 + a + a*a*(1 + a*x) = 1 + a + a*a + a*a*a*x
...
x = 1 + a + a*a + a*a*a + a*a*a*a...x = 1 + a*x
x = 1 + a*(1 + a*x) = 1 + a + a*a*x
x = 1 + a + a*a*(1 + a*x) = 1 + a + a*a + a*a*a*x
...
x = 1 + a + a*a + a*a*a + a*a*a*a...
我们最终得到乘积(元组)的无限和,可以将其解释为:列表要么为空,要么为空1。或单身人士,a; 或一对,a*a;或三元组,a*a*a;等等……嗯,这正是列表的含义——一串as!
We end up with an infinite sum of products (tuples), which can be interpreted as: A list is either empty, 1; or a singleton, a; or a pair, a*a; or a triple, a*a*a; etc… Well, that’s exactly what a list is — a string of as!
列表的含义远不止于此,在了解函子和不动点之后,我们将回到列表和其他递归数据结构。
There’s much more to lists than that, and we’ll come back to them and other recursive data structures after we learn about functors and fixed points.
用符号变量求解方程——这就是代数!这就是这些类型的名称:代数数据类型。
Solving equations with symbolic variables — that’s algebra! It’s what gives these types their name: algebraic data types.
最后,我应该提到类型代数的一个非常重要的解释。a请注意,两种类型和的乘积b必须同时包含 type 的值a 和type 的值b,这意味着两种类型都必须存在。另一方面,两种类型的总和包含 type 的值a 或type 的值b,因此只要其中一个存在就足够了。逻辑and和or也形成一个半环,它也可以映射到类型论中:
Finally, I should mention one very important interpretation of the algebra of types. Notice that a product of two types a and b must contain both a value of type a and a value of type b, which means both types must be inhabited. A sum of two types, on the other hand, contains either a value of type a or a value of type b, so it’s enough if one of them is inhabited. Logical and and or also form a semiring, and it too can be mapped into type theory:
| 逻辑 | 类型 |
|---|---|
| 错误的 | Void |
| 真的 | () |
| 一个 || 乙 | Either a b = Left a | Right b |
| 一个&&b | (a, b) |
这个类比更深入,是逻辑和类型理论之间的库里-霍华德同构的基础。当我们讨论函数类型时,我们会回到它。
This analogy goes deeper, and is the basis of the Curry-Howard isomorphism between logic and type theory. We’ll come back to it when we talk about function types.
Maybe a证明和之间的同构Either () a。Maybe a and Either () a.这是 Haskell 中定义的 sum 类型:
data Shape = Circle Float
| Rect Float Float
当我们想要定义一个作用于areaa 的函数时Shape,我们通过对两个构造函数进行模式匹配来实现:
area :: Shape -> Float
area (Circle r) = pi * r * r
area (Rect d h) = d * h
在 C++ 或 Java 中实现Shape为接口并创建两个类:Circle和Rect。area作为虚函数实现。
Here’s a sum type defined in Haskell:
data Shape = Circle Float
| Rect Float Float
When we want to define a function like area that acts on a Shape, we do it by pattern matching on the two constructors:
area :: Shape -> Float
area (Circle r) = pi * r * r
area (Rect d h) = d * h
Implement Shape in C++ or Java as an interface and create two classes: Circle and Rect. Implement area as a virtual function.
继续前面的示例:我们可以轻松添加一个circ计算 a 周长的新函数Shape。我们可以在不触及 的定义的情况下做到这一点Shape:
circ :: Shape -> Float
circ (Circle r) = 2.0 * pi * r
circ (Rect d h) = 2.0 * (d + h)
添加circ到您的 C++ 或 Java 实现中。您必须接触原始代码的哪些部分?
Continuing with the previous example: We can easily add a new function circ that calculates the circumference of a Shape. We can do it without touching the definition of Shape:
circ :: Shape -> Float
circ (Circle r) = 2.0 * pi * r
circ (Rect d h) = 2.0 * (d + h)
Add circ to your C++ or Java implementation. What parts of the original code did you have to touch?
Square并Shape进行所有必要的更新。在 Haskell 中与 C++ 或 Java 相比,你必须接触哪些代码?(即使您不是 Haskell 程序员,修改也应该非常明显。)Square, to Shape and make all the necessary updates. What code did you have to touch in Haskell vs. C++ or Java? (Even if you’re not a Haskell programmer, the modifications should be pretty obvious.)a + a = 2 * a适用于类型(直至同构)。请记住,根据我们的翻译表,2对应于。Boola + a = 2 * a holds for types (up to isomorphism). Remember that 2 corresponds to Bool, according to our translation table.感谢 Gershom Bazerman 审阅这篇文章并提出有用的评论。
Thanks go to Gershom Bazerman for reviewing this post and helpful comments.
冒着听起来像是破纪录的风险,我要这样说关于函子:函子是一个非常简单但强大的想法。范畴论充满了那些简单但强大的想法。函子是范畴之间的映射。给定两个范畴 C 和 D,函子 F 将 C 中的对象映射到 D 中的对象——它是对象上的函数。如果a是 C 中的对象,我们将把它在 D 中的图像写为F a(无括号)。但范畴不仅仅是对象——它是对象和连接它们的态射。函子也映射态射——它是态射的函数。但它不会随意映射态射——它保留了联系。因此,如果C 中的态射f将对象a与对象b连接起来,
At the risk of sounding like a broken record, I will say this about functors: A functor is a very simple but powerful idea. Category theory is just full of those simple but powerful ideas. A functor is a mapping between categories. Given two categories, C and D, a functor F maps objects in C to objects in D — it’s a function on objects. If a is an object in C, we’ll write its image in D as F a (no parentheses). But a category is not just objects — it’s objects and morphisms that connect them. A functor also maps morphisms — it’s a function on morphisms. But it doesn’t map morphisms willy-nilly — it preserves connections. So if a morphism f in C connects object a to object b,
f :: a -> bf :: a -> b
D 中f的图像F f会将a的图像连接到b的图像:
the image of f in D, F f, will connect the image of a to the image of b:
F f :: F a -> F bF f :: F a -> F b
(这是数学和 Haskell 表示法的混合,希望现在能有意义。在将函子应用于对象或态射时,我不会使用括号。)
(This is a mixture of mathematical and Haskell notation that hopefully makes sense by now. I won’t use parentheses when applying functors to objects or morphisms.)
正如您所看到的,函子保留了范畴的结构:在一个范畴中连接的内容将在另一个范畴中连接。但是范畴的结构还有更多的东西:还有态射的组合。如果h是f和g的组合:
As you can see, a functor preserves the structure of a category: what’s connected in one category will be connected in the other category. But there’s something more to the structure of a category: there’s also the composition of morphisms. If h is a composition of f and g:
h = g . fh = g . f
我们希望 F 下的图像是f和g图像的合成:
we want its image under F to be a composition of the images of f and g:
F h = F g . F fF h = F g . F f
最后,我们希望 C 中的所有恒等态射都映射到 D 中的恒等态射:
Finally, we want all identity morphisms in C to be mapped to identity morphisms in D:
F ida = idF aF ida = idF a
这里,id a是对象a处的标识,id F a是F a处的标识。
Here, ida is the identity at the object a, and idF a the identity at F a.
请注意,这些条件使函子比常规函数更具限制性。函子必须保留范畴的结构。如果将范畴想象为由态射网络连接在一起的对象集合,则不允许函子在该结构中引入任何撕裂。它可以将物体粉碎在一起,它可以将多个态射粘合成一个,但它可能永远不会将物体分开。这种无撕裂约束类似于您从微积分中可能知道的连续性条件。从这个意义上说,函子是“连续的”(尽管函子存在更严格的连续性概念)。就像函数一样,函子可以进行折叠和嵌入。当源范畴远小于目标范畴时,嵌入方面更加突出。在极端情况下,源可以是平凡的单例范畴——具有一个对象和一个态射(恒等)的范畴。从单例范畴到任何其他范畴的函子只需选择该范畴中的一个对象。这完全类似于从单例集合中选择目标集合中的元素的态射的性质。最大崩溃函子称为常数函子 Δ c。它将源范畴中的每个对象映射到目标范畴中的一个选定对象c 。它还将源范畴中的每个态射映射到恒等态射id c。它就像一个黑洞,将一切压缩成一个奇点。当我们讨论极限和余极限时,我们会更多地看到这个函子。
Note that these conditions make functors much more restrictive than regular functions. Functors must preserve the structure of a category. If you picture a category as a collection of objects held together by a network of morphisms, a functor is not allowed to introduce any tears into this fabric. It may smash objects together, it may glue multiple morphisms into one, but it may never break things apart. This no-tearing constraint is similar to the continuity condition you might know from calculus. In this sense functors are “continuous” (although there exists an even more restrictive notion of continuity for functors). Just like functions, functors may do both collapsing and embedding. The embedding aspect is more prominent when the source category is much smaller than the target category. In the extreme, the source can be the trivial singleton category — a category with one object and one morphism (the identity). A functor from the singleton category to any other category simply selects an object in that category. This is fully analogous to the property of morphisms from singleton sets selecting elements in target sets. The maximally collapsing functor is called the constant functor Δc. It maps every object in the source category to one selected object c in the target category. It also maps every morphism in the source category to the identity morphism idc. It acts like a black hole, compacting everything into one singularity. We’ll see more of this functor when we discuss limits and colimits.
让我们脚踏实地地谈谈编程吧。我们有自己的类型和功能范畴。我们可以讨论将这个范畴映射到自身的函子——这样的函子称为endofunctor。那么类型范畴中的内函子是什么?首先,它将类型映射到类型。我们已经看到过此类映射的示例,但也许没有意识到它们就是这样。我正在谈论由其他类型参数化的类型的定义。让我们看几个例子。
Let’s get down to earth and talk about programming. We have our category of types and functions. We can talk about functors that map this category into itself — such functors are called endofunctors. So what’s an endofunctor in the category of types? First of all, it maps types to types. We’ve seen examples of such mappings, maybe without realizing that they were just that. I’m talking about definitions of types that were parameterized by other types. Let’s see a few examples.
的定义是从类型到类型的Maybe映射:aMaybe a
The definition of Maybe is a mapping from type a to type Maybe a:
data Maybe a = Nothing | Just adata Maybe a = Nothing | Just a
这里有一个重要的微妙之处:Maybe它本身不是类型,它是类型构造函数。您必须给它一个类型参数,例如Intor Bool,才能将其转换为类型。Maybe不带任何参数表示类型上的函数。但我们可以变成Maybe一个函子吗?(从现在开始,当我在编程中谈到函子时,我几乎总是指endofunctors。)函子不仅是对象(这里是类型)的映射,也是态射(这里是函数)的映射。a对于从到 的任何函数b:
Here’s an important subtlety: Maybe itself is not a type, it’s a type constructor. You have to give it a type argument, like Int or Bool, in order to turn it into a type. Maybe without any argument represents a function on types. But can we turn Maybe into a functor? (From now on, when I speak of functors in the context of programming, I will almost always mean endofunctors.) A functor is not only a mapping of objects (here, types) but also a mapping of morphisms (here, functions). For any function from a to b:
f :: a -> bf :: a -> b
我们想要生成一个从Maybe a到 的函数Maybe b。为了定义这样的函数,我们需要考虑两种情况,对应于 的两个构造函数Maybe。情况Nothing很简单:我们就回来Nothing。如果参数是Just,我们将将该函数应用于f其内容。f所以下图Maybe就是函数:
we would like to produce a function from Maybe a to Maybe b. To define such a function, we’ll have two cases to consider, corresponding to the two constructors of Maybe. The Nothing case is simple: we’ll just return Nothing back. And if the argument is Just, we’ll apply the function f to its contents. So the image of f under Maybe is the function:
f’ :: Maybe a -> Maybe b
f’ Nothing = Nothing
f’ (Just x) = Just (f x)f’ :: Maybe a -> Maybe b
f’ Nothing = Nothing
f’ (Just x) = Just (f x)
(顺便说一句,在 Haskell 中,您可以在变量名称中使用撇号,这在此类情况下非常方便。)在 Haskell 中,我们将函子的态射映射部分实现为一个名为 的高阶函数fmap。在 的情况下Maybe,它具有以下签名:
(By the way, in Haskell you can use apostrophes in variables names, which is very handy in cases like these.) In Haskell, we implement the morphism-mapping part of a functor as a higher order function called fmap. In the case of Maybe, it has the following signature:
fmap :: (a -> b) -> (Maybe a -> Maybe b)fmap :: (a -> b) -> (Maybe a -> Maybe b)
我们常说fmap 提升函数。提升函数作用于Maybe值。与往常一样,由于柯里化,这个签名可以用两种方式解释:作为一个参数的函数(它本身就是一个函数)(a->b)返回一个函数(Maybe a -> Maybe b);或作为两个参数的函数返回Maybe b:
We often say that fmap lifts a function. The lifted function acts on Maybe values. As usual, because of currying, this signature may be interpreted in two ways: as a function of one argument — which itself is a function (a->b) — returning a function (Maybe a -> Maybe b); or as a function of two arguments returning Maybe b:
fmap :: (a -> b) -> Maybe a -> Maybe bfmap :: (a -> b) -> Maybe a -> Maybe b
根据我们之前的讨论,我们是这样实现fmap的Maybe:
Based on our previous discussion, this is how we implement fmap for Maybe:
fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)
为了证明类型构造函数Maybe与函数一起fmap形成函子,我们必须证明它fmap保留了同一性和组合性。这些被称为“函子法则”,但它们只是确保范畴结构的保存。
To show that the type constructor Maybe together with the function fmap form a functor, we have to prove that fmap preserves identity and composition. These are called “the functor laws,” but they simply ensure the preservation of the structure of the category.
为了证明函子定律,我将使用等式推理,这是 Haskell 中常见的证明技术。它利用了 Haskell 函数被定义为等式的事实:左侧等于右侧。您始终可以用一个替换另一个,可能会重命名变量以避免名称冲突。可以将其视为内联函数,或者相反,将表达式重构为函数。我们以恒等函数为例:
To prove the functor laws, I will use equational reasoning, which is a common proof technique in Haskell. It takes advantage of the fact that Haskell functions are defined as equalities: the left hand side equals the right hand side. You can always substitute one for another, possibly renaming variables to avoid name conflicts. Think of this as either inlining a function, or the other way around, refactoring an expression into a function. Let’s take the identity function as an example:
id x = xid x = x
例如,如果您id y在某些表达式中看到,您可以将其替换为y(内联)。此外,如果您看到id应用于表达式,例如id (y + 2),您可以将其替换为表达式本身(y + 2)。这种替换是双向的:您可以e用id e(重构)替换任何表达式。如果函数是通过模式匹配定义的,则可以独立使用每个子定义。例如,根据上面的定义,fmap您可以替换fmap f Nothing为Nothing,或者反之亦然。让我们看看这在实践中是如何运作的。让我们从身份的保存开始:
If you see, for instance, id y in some expression, you can replace it with y (inlining). Further, if you see id applied to an expression, say id (y + 2), you can replace it with the expression itself (y + 2). And this substitution works both ways: you can replace any expression e with id e (refactoring). If a function is defined by pattern matching, you can use each sub-definition independently. For instance, given the above definition of fmap you can replace fmap f Nothing with Nothing, or the other way around. Let’s see how this works in practice. Let’s start with the preservation of identity:
fmap id = idfmap id = id
有两种情况需要考虑:Nothing和Just。这是第一种情况(我使用 Haskell 伪代码将左侧转换为右侧):
There are two cases to consider: Nothing and Just. Here’s the first case (I’m using Haskell pseudo-code to transform the left hand side to the right hand side):
fmap id Nothing
= { definition of fmap }
Nothing
= { definition of id }
id Nothing fmap id Nothing
= { definition of fmap }
Nothing
= { definition of id }
id Nothing
请注意,在最后一步中我使用了向后的定义id。我将表达式替换Nothing为id Nothing. 在实践中,你通过“蜡烛两端燃烧”来进行这样的证明,直到你在中间遇到相同的表达式 - 就是这样Nothing。第二种情况也很简单:
Notice that in the last step I used the definition of id backwards. I replaced the expression Nothing with id Nothing. In practice, you carry out such proofs by “burning the candle at both ends,” until you hit the same expression in the middle — here it was Nothing. The second case is also easy:
fmap id (Just x)
= { definition of fmap }
Just (id x)
= { definition of id }
Just x
= { definition of id }
id (Just x) fmap id (Just x)
= { definition of fmap }
Just (id x)
= { definition of id }
Just x
= { definition of id }
id (Just x)
现在,让我们证明fmap保留构图:
Now, lets show that fmap preserves composition:
fmap (g . f) = fmap g . fmap ffmap (g . f) = fmap g . fmap f
首先是Nothing案例:
First the Nothing case:
fmap (g . f) Nothing
= { definition of fmap }
Nothing
= { definition of fmap }
fmap g Nothing
= { definition of fmap }
fmap g (fmap f Nothing) fmap (g . f) Nothing
= { definition of fmap }
Nothing
= { definition of fmap }
fmap g Nothing
= { definition of fmap }
fmap g (fmap f Nothing)
然后是Just案例:
And then the Just case:
fmap (g . f) (Just x)
= { definition of fmap }
Just ((g . f) x)
= { definition of composition }
Just (g (f x))
= { definition of fmap }
fmap g (Just (f x))
= { definition of fmap }
fmap g (fmap f (Just x))
= { definition of composition }
(fmap g . fmap f) (Just x) fmap (g . f) (Just x)
= { definition of fmap }
Just ((g . f) x)
= { definition of composition }
Just (g (f x))
= { definition of fmap }
fmap g (Just (f x))
= { definition of fmap }
fmap g (fmap f (Just x))
= { definition of composition }
(fmap g . fmap f) (Just x)
值得强调的是,等式推理不适用于具有副作用的 C++ 风格“函数”。考虑这段代码:
It’s worth stressing that equational reasoning doesn’t work for C++ style “functions” with side effects. Consider this code:
int square(int x) {
return x * x;
}
int counter() {
static int c = 0;
return c++;
}
double y = square(counter());int square(int x) {
return x * x;
}
int counter() {
static int c = 0;
return c++;
}
double y = square(counter());
使用等式推理,您将能够内联square得到:
Using equational reasoning, you would be able to inline square to get:
double y = counter() * counter();double y = counter() * counter();
这绝对不是一个有效的转换,并且不会产生相同的结果。square尽管如此,如果您实现为宏,C++ 编译器将尝试使用等式推理,从而带来灾难性的结果。
This is definitely not a valid transformation, and it will not produce the same result. Despite that, the C++ compiler will try to use equational reasoning if you implement square as a macro, with disastrous results.
函子很容易在 Haskell 中表达,但它们可以用任何支持泛型编程和高阶函数的语言定义。让我们考虑一下 C++ 的类似物Maybe,即模板类型optional。下面是实现的草图(实际实现要复杂得多,处理参数传递的各种方式、复制语义以及 C++ 的资源管理问题特征):
Functors are easily expressed in Haskell, but they can be defined in any language that supports generic programming and higher-order functions. Let’s consider the C++ analog of Maybe, the template type optional. Here’s a sketch of the implementation (the actual implementation is much more complex, dealing with various ways the argument may be passed, with copy semantics, and with the resource management issues characteristic of C++):
template<class T>
class optional {
bool _isValid; // the tag
T _v;
public:
optional() : _isValid(false) {} // Nothing
optional(T x) : _isValid(true) , _v(x) {} // Just
bool isValid() const { return _isValid; }
T val() const { return _v; }
};template<class T>
class optional {
bool _isValid; // the tag
T _v;
public:
optional() : _isValid(false) {} // Nothing
optional(T x) : _isValid(true) , _v(x) {} // Just
bool isValid() const { return _isValid; }
T val() const { return _v; }
};
该模板提供了函子定义的一部分:类型的映射。它将任何类型映射T到新类型optional<T>。让我们定义它对函数的操作:
This template provides one part of the definition of a functor: the mapping of types. It maps any type T to a new type optional<T>. Let’s define its action on functions:
template<class A, class B>
std::function<optional<B>(optional<A>)>
fmap(std::function<B(A)> f)
{
return [f](optional<A> opt) {
if (!opt.isValid())
return optional<B>{};
else
return optional<B>{ f(opt.val()) };
};
}template<class A, class B>
std::function<optional<B>(optional<A>)>
fmap(std::function<B(A)> f)
{
return [f](optional<A> opt) {
if (!opt.isValid())
return optional<B>{};
else
return optional<B>{ f(opt.val()) };
};
}
这是一个高阶函数,以函数作为参数并返回函数。这是它的未柯里化版本:
This is a higher order function, taking a function as an argument and returning a function. Here’s the uncurried version of it:
template<class A, class B>
optional<B> fmap(std::function<B(A)> f, optional<A> opt) {
if (!opt.isValid())
return optional<B>{};
else
return optional<B>{ f(opt.val()) };
}template<class A, class B>
optional<B> fmap(std::function<B(A)> f, optional<A> opt) {
if (!opt.isValid())
return optional<B>{};
else
return optional<B>{ f(opt.val()) };
}
还可以选择制作 的fmap模板方法optional。这种尴尬的选择使得在 C++ 中抽象函子模式成为一个问题。仿函数应该是一个继承的接口(不幸的是,你不能有模板虚拟函数)?它应该是柯里化的还是非柯里化的自由模板函数?C++ 编译器能否正确推断缺失的类型,还是应该显式指定它们?f考虑输入函数将 an 转换int为 a的情况bool。编译器如何确定以下类型g:
There is also an option of making fmap a template method of optional. This embarrassment of choices makes abstracting the functor pattern in C++ a problem. Should functor be an interface to inherit from (unfortunately, you can’t have template virtual functions)? Should it be a curried or an uncurried free template function? Can the C++ compiler correctly infer the missing types, or should they be specified explicitly? Consider a situation where the input function f takes an int to a bool. How will the compiler figure out the type of g:
auto g = fmap(f);auto g = fmap(f);
特别是如果将来有多个函子重载fmap? (我们很快就会看到更多函子。)
especially if, in the future, there are multiple functors overloading fmap? (We’ll see more functors soon.)
那么 Haskell 如何处理抽象函子呢?它使用类型类机制。类型类定义了支持公共接口的一系列类型。例如,支持相等的对象类定义如下:
So how does Haskell deal with abstracting the functor? It uses the typeclass mechanism. A typeclass defines a family of types that support a common interface. For instance, the class of objects that support equality is defined as follows:
class Eq a where
(==) :: a -> a -> Boolclass Eq a where
(==) :: a -> a -> Bool
此定义指出,如果类型支持a采用两个类型参数并返回 a 的运算符,则该类型属于该类。如果你想告诉 Haskell 某个特定类型是,你必须将其声明为此类的实例并提供 的实现。例如,给定 2D 的定义(两个 s 的产品类型):Eq(==)aBoolEq(==)PointFloat
This definition states that type a is of the class Eq if it supports the operator (==) that takes two arguments of type a and returns a Bool. If you want to tell Haskell that a particular type is Eq, you have to declare it an instance of this class and provide the implementation of (==). For example, given the definition of a 2D Point (a product type of two Floats):
data Point = Pt Float Floatdata Point = Pt Float Float
您可以定义点的相等性:
you can define the equality of points:
instance Eq Point where
(Pt x y) == (Pt x' y') = x == x' && y == y'instance Eq Point where
(Pt x y) == (Pt x' y') = x == x' && y == y'
在这里,我在两个模式和(==)之间的中缀位置使用了运算符(我定义的运算符) 。函数体遵循单个等号。一旦声明为 的实例,您就可以直接比较点是否相等。请注意,与 C++ 或 Java 不同,您不必在定义时指定类(或接口)——您可以稍后在客户端代码中指定。类型类也是 Haskell 重载函数(和运算符)的唯一机制。我们需要它来重载不同的函子。不过,有一个复杂之处:函子没有定义为类型,而是定义为类型的映射,即类型构造函数。我们需要的类型类不是类型族(如 的情况),而是类型构造函数族。幸运的是,Haskell 类型类可以与类型构造函数一起使用,也可以与类型一起使用。所以这是该类的定义:(Pt x y)(Pt x' y')PointEqEqPointfmapEqFunctor
Here I used the operator (==) (the one I’m defining) in the infix position between the two patterns (Pt x y) and (Pt x' y'). The body of the function follows the single equal sign. Once Point is declared an instance of Eq, you can directly compare points for equality. Notice that, unlike in C++ or Java, you don’t have to specify the Eq class (or interface) when defining Point — you can do it later in client code. Typeclasses are also Haskell’s only mechanism for overloading functions (and operators). We will need that for overloading fmap for different functors. There is one complication, though: a functor is not defined as a type but as a mapping of types, a type constructor. We need a typeclass that’s not a family of types, as was the case with Eq, but a family of type constructors. Fortunately a Haskell typeclass works with type constructors as well as with types. So here’s the definition of the Functor class:
class Functor f where
fmap :: (a -> b) -> f a -> f bclass Functor f where
fmap :: (a -> b) -> f a -> f b
它规定如果存在具有指定类型签名的函数,则为fa 。小写的是类型变量,类似于类型变量和。然而,编译器可以通过查看其用法来推断它表示类型构造函数而不是类型:作用于其他类型,如 和中。因此,在声明 的实例时,必须为其提供类型构造函数,如下所示:Functorfmapfabf af bFunctorMaybe
It stipulates that f is a Functor if there exists a function fmap with the specified type signature. The lowercase f is a type variable, similar to type variables a and b. The compiler, however, is able to deduce that it represents a type constructor rather than a type by looking at its usage: acting on other types, as in f a and f b. Accordingly, when declaring an instance of Functor, you have to give it a type constructor, as is the case with Maybe:
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just x) = Just (f x)
顺便说一句,该类Functor及其许多简单数据类型(包括 )的实例定义Maybe是标准 Prelude 库的一部分。
By the way, the Functor class, as well as its instance definitions for a lot of simple data types, including Maybe, are part of the standard Prelude library.
我们可以在 C++ 中尝试同样的方法吗?类型构造函数对应于模板类,所以optional以此类推,我们可以fmap使用模板模板参数 F来进行参数化。这是它的语法:
Can we try the same approach in C++? A type constructor corresponds to a template class, like optional, so by analogy, we would parameterize fmap with a template template parameter F. This is the syntax for it:
template<template<class> F, class A, class B>
F<B> fmap(std::function<B(A)>, F<A>);template<template<class> F, class A, class B>
F<B> fmap(std::function<B(A)>, F<A>);
我们希望能够针对不同的函子专门设计这个模板。不幸的是,C++ 中禁止模板函数的部分特化。你不能写:
We would like to be able to specialize this template for different functors. Unfortunately, there is a prohibition against partial specialization of template functions in C++. You can’t write:
template<class A, class B>
optional<B> fmap<optional>(std::function<B(A)> f, optional<A> opt)template<class A, class B>
optional<B> fmap<optional>(std::function<B(A)> f, optional<A> opt)
相反,我们必须依靠函数重载,这让我们回到了 uncurried 的原始定义fmap:
Instead, we have to fall back on function overloading, which brings us back to the original definition of the uncurried fmap:
template<class A, class B>
optional<B> fmap(std::function<B(A)> f, optional<A> opt)
{
if (!opt.isValid())
return optional<B>{};
else
return optional<B>{ f(opt.val()) };
}template<class A, class B>
optional<B> fmap(std::function<B(A)> f, optional<A> opt)
{
if (!opt.isValid())
return optional<B>{};
else
return optional<B>{ f(opt.val()) };
}
这个定义有效,但只是因为 的第二个参数fmap选择了重载。它完全忽略了更通用的定义fmap。
This definition works, but only because the second argument of fmap selects the overload. It totally ignores the more generic definition of fmap.
为了对函子在编程中的作用有一些直观的了解,我们需要看更多的例子。由另一种类型参数化的任何类型都是函子的候选者。通用容器通过它们存储的元素的类型进行参数化,所以让我们看一个非常简单的容器,即列表:
To get some intuition as to the role of functors in programming, we need to look at more examples. Any type that is parameterized by another type is a candidate for a functor. Generic containers are parameterized by the type of the elements they store, so let’s look at a very simple container, the list:
data List a = Nil | Cons a (List a)data List a = Nil | Cons a (List a)
我们有类型构造函数,它是从任何类型到该类型的List映射。为了表明这是一个函子,我们必须定义函数的提升:给定一个函数,定义一个函数:aList aLista->bList a -> List b
We have the type constructor List, which is a mapping from any type a to the type List a. To show that List is a functor we have to define the lifting of functions: Given a function a->b define a function List a -> List b:
fmap :: (a -> b) -> (List a -> List b)fmap :: (a -> b) -> (List a -> List b)
作用于的函数List a必须考虑与两个列表构造函数相对应的两种情况。这个Nil例子很简单——只需返回Nil——对于空列表你无能为力。这个Cons例子有点棘手,因为它涉及到递归。因此,让我们退后一步,考虑一下我们正在尝试做什么。我们有一个 的列表,一个转换为 的a函数,并且我们想要生成一个 的列表。显而易见的事情是使用将列表中的每个元素从变为。考虑到(非空)列表被定义为头和尾,我们在实践中如何做到这一点?我们应用于头部,并将抬起的(ped)应用于尾部。这是一个递归定义,因为我们用 lift 来定义lift :fabbfabConsffmapfff
A function acting on List a must consider two cases corresponding to the two list constructors. The Nil case is trivial — just return Nil — there isn’t much you can do with an empty list. The Cons case is a bit tricky, because it involves recursion. So let’s step back for a moment and consider what we are trying to do. We have a list of a, a function f that turns a to b, and we want to generate a list of b. The obvious thing is to use f to turn each element of the list from a to b. How do we do this in practice, given that a (non-empty) list is defined as the Cons of a head and a tail? We apply f to the head and apply the lifted (fmapped) f to the tail. This is a recursive definition, because we are defining lifted f in terms of lifted f:
fmap f (Cons x t) = Cons (f x) (fmap f t)fmap f (Cons x t) = Cons (f x) (fmap f t)
请注意,在右侧,fmap f应用于比我们定义它的列表短的列表 - 它应用于其尾部。我们递归到越来越短的列表,所以我们最终一定会到达空列表,或者Nil。但正如我们之前决定的那样,fmap f对Nilreturns进行操作Nil,从而终止递归。为了获得最终结果,我们使用构造函数将新的头部(f x)和新的尾部组合起来。将它们放在一起,这是列表函子的实例声明:(fmap f t)Cons
Notice that, on the right hand side, fmap f is applied to a list that’s shorter than the list for which we are defining it — it’s applied to its tail. We recurse towards shorter and shorter lists, so we are bound to eventually reach the empty list, or Nil. But as we’ve decided earlier, fmap f acting on Nil returns Nil, thus terminating the recursion. To get the final result, we combine the new head (f x) with the new tail (fmap f t) using the Cons constructor. Putting it all together, here’s the instance declaration for the list functor:
instance Functor List where
fmap _ Nil = Nil
fmap f (Cons x t) = Cons (f x) (fmap f t)instance Functor List where
fmap _ Nil = Nil
fmap f (Cons x t) = Cons (f x) (fmap f t)
如果您更熟悉 C++,请考虑 a 的情况std::vector,它可以被视为最通用的 C++ 容器。fmapfor的实现std::vector只是一个简单的封装std::transform:
If you are more comfortable with C++, consider the case of a std::vector, which could be considered the most generic C++ container. The implementation of fmap for std::vector is just a thin encapsulation of std::transform:
template<class A, class B>
std::vector<B> fmap(std::function<B(A)> f, std::vector<A> v)
{
std::vector<B> w;
std::transform( std::begin(v)
, std::end(v)
, std::back_inserter(w)
, f);
return w;
}template<class A, class B>
std::vector<B> fmap(std::function<B(A)> f, std::vector<A> v)
{
std::vector<B> w;
std::transform( std::begin(v)
, std::end(v)
, std::back_inserter(w)
, f);
return w;
}
例如,我们可以使用它来计算数字序列的元素的平方:
We can use it, for instance, to square the elements of a sequence of numbers:
std::vector<int> v{ 1, 2, 3, 4 };
auto w = fmap([](int i) { return i*i; }, v);
std::copy( std::begin(w)
, std::end(w)
, std::ostream_iterator(std::cout, ", "));std::vector<int> v{ 1, 2, 3, 4 };
auto w = fmap([](int i) { return i*i; }, v);
std::copy( std::begin(w)
, std::end(w)
, std::ostream_iterator(std::cout, ", "));
大多数 C++ 容器都是函子,因为它们实现了可以传递给 的迭代器std::transform,而 是 的更原始的表亲fmap。不幸的是,函子的简单性在迭代器和临时变量的常见混乱中消失了(参见上面的实现fmap)。我很高兴地说,新提出的 C++ 范围库使范围的函数性质更加明显。
Most C++ containers are functors by virtue of implementing iterators that can be passed to std::transform, which is the more primitive cousin of fmap. Unfortunately, the simplicity of a functor is lost under the usual clutter of iterators and temporaries (see the implementation of fmap above). I’m happy to say that the new proposed C++ range library makes the functorial nature of ranges much more pronounced.
现在您可能已经有了一些直觉——例如,函子是某种容器——让我向您展示一个乍一看看起来非常不同的示例。a考虑类型到返回函数类型的映射a。我们还没有真正深入讨论函数类型——完整的分类处理即将到来——但我们作为程序员对这些有一些了解。在 Haskell 中,函数类型是使用箭头类型构造函数构造的,(->)该构造函数采用两种类型:参数类型和结果类型。您已经见过它的中缀形式 ,a->b但它同样可以用括号形式以前缀形式使用:
Now that you might have developed some intuitions — for instance, functors being some kind of containers — let me show you an example which at first sight looks very different. Consider a mapping of type a to the type of a function returning a. We haven’t really talked about function types in depth — the full categorical treatment is coming — but we have some understanding of those as programmers. In Haskell, a function type is constructed using the arrow type constructor (->) which takes two types: the argument type and the result type. You’ve already seen it in infix form, a->b, but it can equally well be used in prefix form, when parenthesized:
(->) a b(->) a b
就像常规函数一样,可以部分应用具有多个参数的类型函数。因此,当我们只向箭头提供一种类型参数时,它仍然需要另一种类型参数。这就是为什么:
Just like with regular functions, type functions of more than one argument can be partially applied. So when we provide just one type argument to the arrow, it still expects another one. That’s why:
(->) a(->) a
是一个类型构造函数。它需要再一个类型b来生成一个完整的类型a->b。就目前而言,它定义了一整套由 参数化的类型构造函数a。让我们看看这是否也是一个函子家族。处理两个类型参数可能会有点混乱,所以让我们进行一些重命名。让我们将参数类型r和结果类型称为a,与我们之前的函子定义一致。所以我们的类型构造函数接受任何类型a并将其映射到 type r->a。为了表明它是一个函子,我们希望将一个函数提升为一个接受并返回 的a->b函数。这些是使用分别作用于和的类型构造函数形成的类型。这是应用于本例的类型签名:r->ar->b(->) rabfmap
is a type constructor. It needs one more type b to produce a complete type a->b. As it stands, it defines a whole family of type constructors parameterized by a. Let’s see if this is also a family of functors. Dealing with two type parameters can get a bit confusing, so let’s do some renaming. Let’s call the argument type r and the result type a, in line with our previous functor definitions. So our type constructor takes any type a and maps it into the type r->a. To show that it’s a functor, we want to lift a function a->b to a function that takes r->a and returns r->b. These are the types that are formed using the type constructor (->) r acting on, respectively, a and b. Here’s the type signature of fmap applied to this case:
fmap :: (a -> b) -> (r -> a) -> (r -> b)fmap :: (a -> b) -> (r -> a) -> (r -> b)
我们必须解决以下难题:给定一个函数f::a->b和一个函数g::r->a,创建一个函数r->b。我们只有一种方法可以组合这两个函数,并且结果正是我们所需要的。这是我们的实现fmap:
We have to solve the following puzzle: given a function f::a->b and a function g::r->a, create a function r->b. There is only one way we can compose the two functions, and the result is exactly what we need. So here’s the implementation of our fmap:
instance Functor ((->) r) where
fmap f g = f . ginstance Functor ((->) r) where
fmap f g = f . g
它就是有效的!如果您喜欢简洁的表示法,则可以通过注意到组合可以以前缀形式重写来进一步简化此定义:
It just works! If you like terse notation, this definition can be reduced further by noticing that composition can be rewritten in prefix form:
fmap f g = (.) f gfmap f g = (.) f g
并且可以省略参数以产生两个函数的直接相等:
and the arguments can be omitted to yield a direct equality of two functions:
fmap = (.)fmap = (.)
类型构造函数(->) r与上面的实现的这种组合fmap称为读取器函子。
This combination of the type constructor (->) r with the above implementation of fmap is called the reader functor.
我们已经在编程语言中看到了一些函子的示例,它们定义了通用容器,或者至少是包含其参数化类型的某些值的对象。读者函子似乎是一个异常值,因为我们不将函数视为数据。但我们已经看到纯函数可以被记忆,并且函数执行可以变成查表。表格是数据。相反,由于 Haskell 的惰性,传统容器(如列表)实际上可能被实现为函数。例如,考虑一个无限的自然数列表,它可以紧凑地定义为:
We’ve seen some examples of functors in programming languages that define general-purpose containers, or at least objects that contain some value of the type they are parameterized over. The reader functor seems to be an outlier, because we don’t think of functions as data. But we’ve seen that pure functions can be memoized, and function execution can be turned into table lookup. Tables are data. Conversely, because of Haskell’s laziness, a traditional container, like a list, may actually be implemented as a function. Consider, for instance, an infinite list of natural numbers, which can be compactly defined as:
nats :: [Integer]
nats = [1..]nats :: [Integer]
nats = [1..]
第一行中,一对方括号是 Haskell 的内置列表类型构造函数。在第二行中,方括号用于创建列表文字。显然,这样的无限列表无法存储在内存中。Integer编译器将其实现为按需生成 s 的函数。Haskell 有效地模糊了数据和代码之间的区别。列表可以被视为函数,函数可以被视为将参数映射到结果的表。如果函数的域是有限的并且不是太大,则后者甚至可以是实用的。然而,实现为表查找是不切实际的strlen,因为存在无限多个不同的字符串。作为程序员,我们不喜欢无穷大,但在范畴论中,你学会把无穷大当早餐吃。无论是所有字符串的集合,还是宇宙所有可能状态(过去、现在和未来)的集合 - 我们都可以处理它!因此,我喜欢将函子对象(由 endofunctor 生成的类型的对象)视为包含参数化类型的一个或多个值,即使这些值实际上并不存在于其中。函子的一个例子是 C++ std::future,它可能在某个时刻包含一个值,但不能保证它会包含一个值;如果你想访问它,你可以阻塞等待另一个线程完成执行。另一个例子是 HaskellIO对象,它可能包含用户输入,或者带有“Hello World!”的宇宙的未来版本。显示在监视器上。根据这种解释,函子对象可能包含其参数化类型的一个或多个值。或者它可能包含生成这些值的方法。我们根本不关心能否访问这些值——这完全是可选的,并且超出了函子的范围。我们感兴趣的是能够使用函数来操纵这些值。如果可以访问这些值,那么我们应该能够看到此操作的结果。如果他们不能,那么我们所关心的是操作是否正确组成,并且使用恒等函数进行的操作不会改变任何东西。只是为了向您展示我们有多么不关心能否访问函子对象内的值,这里有一个完全忽略其参数的类型构造函数a:
In the first line, a pair of square brackets is the Haskell’s built-in type constructor for lists. In the second line, square brackets are used to create a list literal. Obviously, an infinite list like this cannot be stored in memory. The compiler implements it as a function that generates Integers on demand. Haskell effectively blurs the distinction between data and code. A list could be considered a function, and a function could be considered a table that maps arguments to results. The latter can even be practical if the domain of the function is finite and not too large. It would not be practical, however, to implement strlen as table lookup, because there are infinitely many different strings. As programmers, we don’t like infinities, but in category theory you learn to eat infinities for breakfast. Whether it’s a set of all strings or a collection of all possible states of the Universe, past, present, and future — we can deal with it! So I like to think of the functor object (an object of the type generated by an endofunctor) as containing a value or values of the type over which it is parameterized, even if these values are not physically present there. One example of a functor is a C++ std::future, which may at some point contain a value, but it’s not guaranteed it will; and if you want to access it, you may block waiting for another thread to finish execution. Another example is a Haskell IO object, which may contain user input, or the future versions of our Universe with “Hello World!” displayed on the monitor. According to this interpretation, a functor object is something that may contain a value or values of the type it’s parameterized upon. Or it may contain a recipe for generating those values. We are not at all concerned about being able to access the values — that’s totally optional, and outside of the scope of the functor. All we are interested in is to be able to manipulate those values using functions. If the values can be accessed, then we should be able to see the results of this manipulation. If they can’t, then all we care about is that the manipulations compose correctly and that the manipulation with an identity function doesn’t change anything. Just to show you how much we don’t care about being able to access the values inside a functor object, here’s a type constructor that ignores completely its argument a:
data Const c a = Const cdata Const c a = Const c
类型构造函数Const采用两种类型,c和a。就像我们对箭头构造函数所做的那样,我们将部分应用它来创建一个函子。数据构造函数(也称为Const)仅采用一个类型的值c。它不依赖于a. fmap该类型构造函数的类型是:
The Const type constructor takes two types, c and a. Just like we did with the arrow constructor, we are going to partially apply it to create a functor. The data constructor (also called Const) takes just one value of type c. It has no dependence on a. The type of fmap for this type constructor is:
fmap :: (a -> b) -> Const c a -> Const c bfmap :: (a -> b) -> Const c a -> Const c b
因为函子忽略它的类型参数,所以 的实现fmap可以自由地忽略它的函数参数——该函数没有任何可操作的:
Because the functor ignores its type argument, the implementation of fmap is free to ignore its function argument — the function has nothing to act upon:
instance Functor (Const c) where
fmap _ (Const v) = Const vinstance Functor (Const c) where
fmap _ (Const v) = Const v
这在 C++ 中可能会更清楚一些(我从来没想过我会说出这些话!),其中类型参数(编译时)和值(运行时)之间有更明显的区别:
This might be a little clearer in C++ (I never thought I would utter those words!), where there is a stronger distinction between type arguments — which are compile-time — and values, which are run-time:
template<class C, class A>
struct Const {
Const(C v) : _v(v) {}
C _v;
};template<class C, class A>
struct Const {
Const(C v) : _v(v) {}
C _v;
};
的 C++ 实现fmap也会忽略函数参数,并且本质上会重新转换Const参数而不更改其值:
The C++ implementation of fmap also ignores the function argument and essentially re-casts the Const argument without changing its value:
template<class C, class A, class B>
Const<C, B> fmap(std::function<B(A)> f, Const<C, A> c) {
return Const<C, B>{c._v};
}template<class C, class A, class B>
Const<C, B> fmap(std::function<B(A)> f, Const<C, A> c) {
return Const<C, B>{c._v};
}
尽管函子很奇怪,但它Const在许多构造中发挥着重要作用。在范畴论中,它是我之前提到的 Δ c函子的一个特例——黑洞的内函子情况。将来我们会看到更多这样的情况。
Despite its weirdness, the Const functor plays an important role in many constructions. In category theory, it’s a special case of the Δc functor I mentioned earlier — the endo-functor case of a black hole. We’ll be seeing more of it it in the future.
说服自己范畴之间的函子组合并不难,就像集合之间的函数组合一样。两个函子的组合,当作用于对象时,只是它们各自对象映射的组合;当作用于态射时也是如此。在跳过两个函子之后,恒等态射最终成为恒等态射,而态射的组合最终成为态射的组合。确实没有什么太多的。特别是,构造内函子很容易。还记得这个功能maybeTail吗?我将使用 Haskell 的内置列表实现重写它:
It’s not hard to convince yourself that functors between categories compose, just like functions between sets compose. A composition of two functors, when acting on objects, is just the composition of their respective object mappings; and similarly when acting on morphisms. After jumping through two functors, identity morphisms end up as identity morphisms, and compositions of morphisms finish up as compositions of morphisms. There’s really nothing much to it. In particular, it’s easy to compose endofunctors. Remember the function maybeTail? I’ll rewrite it using the Haskell’s built in implementation of lists:
maybeTail :: [a] -> Maybe [a]
maybeTail [] = Nothing
maybeTail (x:xs) = Just xsmaybeTail :: [a] -> Maybe [a]
maybeTail [] = Nothing
maybeTail (x:xs) = Just xs
(我们过去调用的空列表构造函数Nil被空方括号对替换[]。Cons构造函数被中缀运算符:(冒号)替换。) 的结果maybeTail是由两个函子组成的类型,Maybe并且[],上a. 这些函子中的每一个都配备了自己的 版本fmap,但是如果我们想将某些函数应用于f组合的内容:列表,该怎么办Maybe?我们必须突破两层函子。我们可以利用fmap外来突破Maybe。但我们不能只发送f内部Maybe,因为它f不适用于列表。我们必须发送(fmap f)对内部列表进行操作。例如,让我们看看如何对Maybe整数列表的元素进行平方:
(The empty list constructor that we used to call Nil is replaced with the empty pair of square brackets []. The Cons constructor is replaced with the infix operator : (colon).) The result of maybeTail is of a type that’s a composition of two functors, Maybe and [], acting on a. Each of these functors is equipped with its own version of fmap, but what if we want to apply some function f to the contents of the composite: a Maybe list? We have to break through two layers of functors. We can use fmap to break through the outer Maybe. But we can’t just send f inside Maybe because f doesn’t work on lists. We have to send (fmap f) to operate on the inner list. For instance, let’s see how we can square the elements of a Maybe list of integers:
square x = x * x
mis :: Maybe [Int]
mis = Just [1, 2, 3]
mis2 = fmap (fmap square) missquare x = x * x
mis :: Maybe [Int]
mis = Just [1, 2, 3]
mis2 = fmap (fmap square) mis
编译器在分析类型后会发现,对于外部类型fmap,它应该使用实例的实现Maybe,而对于内部类型,它应该使用列表函子实现。上面的代码可以重写为:
The compiler, after analyzing the types, will figure out that, for the outer fmap, it should use the implementation from the Maybe instance, and for the inner one, the list functor implementation. It may not be immediately obvious that the above code may be rewritten as:
mis2 = (fmap . fmap) square mismis2 = (fmap . fmap) square mis
但请记住,这fmap可能被视为只有一个参数的函数:
But remember that fmap may be considered a function of just one argument:
fmap :: (a -> b) -> (f a -> f b)fmap :: (a -> b) -> (f a -> f b)
在我们的例子中,第二个fmap作为(fmap . fmap)它的参数:
In our case, the second fmap in (fmap . fmap) takes as its argument:
square :: Int -> Intsquare :: Int -> Int
并返回一个类型的函数:
and returns a function of the type:
[Int] -> [Int][Int] -> [Int]
然后第一个fmap函数接受该函数并返回一个函数:
The first fmap then takes that function and returns a function:
Maybe [Int] -> Maybe [Int]Maybe [Int] -> Maybe [Int]
最后,将该函数应用于mis. 因此两个函子的复合是一个函子,其fmap是对应的 s 的复合fmap。回到范畴论:很明显,函子组合是结合的(对象的映射是结合的,态射的映射是结合的)。每个范畴中还有一个平凡的恒等函子:它将每个对象映射到自身,并将每个态射映射到自身。因此,函子在某些范畴中具有与态射相同的性质。但那会是什么范畴呢?它必须是一个范畴,其中对象是范畴,态射是函子。这是范畴的范畴。但是所有范畴的范畴必须包含其自身,我们就会陷入同样的悖论,使所有集合的集合变得不可能。然而,所有小范畴中有一个范畴称为Cat(它很大,因此它不能成为其自身的成员)。小范畴是指对象形成一个集合,而不是大于集合的对象。请注意,在范畴论中,即使是无限不可数的集合也被认为是“小”。我想我应该提到这些事情,因为我发现我们能够识别在多个抽象层次上重复的相同结构非常令人惊奇。稍后我们将看到函子也形成范畴。
Finally, that function is applied to mis. So the composition of two functors is a functor whose fmap is the composition of the corresponding fmaps. Going back to category theory: It’s pretty obvious that functor composition is associative (the mapping of objects is associative, and the mapping of morphisms is associative). And there is also a trivial identity functor in every category: it maps every object to itself, and every morphism to itself. So functors have all the same properties as morphisms in some category. But what category would that be? It would have to be a category in which objects are categories and morphisms are functors. It’s a category of categories. But a category of all categories would have to include itself, and we would get into the same kinds of paradoxes that made the set of all sets impossible. There is, however, a category of all small categories called Cat (which is big, so it can’t be a member of itself). A small category is one in which objects form a set, as opposed to something larger than a set. Mind you, in category theory, even an infinite uncountable set is considered “small.” I thought I’d mention these things because I find it pretty amazing that we can recognize the same structures repeating themselves at many levels of abstraction. We’ll see later that functors form categories as well.
我们可以Maybe通过定义将类型构造函数转换为函子吗:
fmap _ _ = Nothing
它忽略了它的两个参数?(提示:检查函子定律。)
Can we turn the Maybe type constructor into a functor by defining:
fmap _ _ = Nothing
which ignores both of its arguments? (Hint: Check the functor laws.)
Gershom Bazerman 很友善地继续审查这些帖子。我很感谢他的耐心和洞察力。
Gershom Bazerman is kind enough to keep reviewing these posts. I’m grateful for his patience and insight.
现在您已经知道什么是函子,并且已经看过一些示例,让我们看看如何从较小的函子构建更大的函子。特别有趣的是,看看哪些类型构造函数(对应于范畴中对象之间的映射)可以扩展到函子(包括态射之间的映射)。
Now that you know what a functor is, and have seen a few examples, let’s see how we can build larger functors from smaller ones. In particular it’s interesting to see which type constructors (which correspond to mappings between objects in a category) can be extended to functors (which include mappings between morphisms).
由于函子是Cat(范畴的范畴)中的态射,因此许多关于态射(尤其是函数)的直觉也适用于函子。例如,就像您可以拥有两个参数的函数一样,您也可以拥有两个参数的函子或bifunctor。在对象上,双函子将每对对象(一个来自范畴 C,一个来自范畴 D)映射到范畴 E 中的一个对象。请注意,这只是说它是从范畴 C×D 到 E 的笛卡尔积的映射。
Since functors are morphisms in Cat (the category of categories), a lot of intuitions about morphisms — and functions in particular — apply to functors as well. For instance, just like you can have a function of two arguments, you can have a functor of two arguments, or a bifunctor. On objects, a bifunctor maps every pair of objects, one from category C, and one from category D, to an object in category E. Notice that this is just saying that it’s a mapping from a cartesian product of categories C×D to E.
这非常简单。但函子性意味着双函子也必须映射态射。不过,这一次,它必须将一对态射(一个来自 C,一个来自 D)映射到 E 中的态射。
That’s pretty straightforward. But functoriality means that a bifunctor has to map morphisms as well. This time, though, it must map a pair of morphisms, one from C and one from D, to a morphism in E.
同样,一对态射只是乘积范畴 C×D 中的一个态射。我们将范畴的笛卡尔积中的态射定义为从一对对象到另一对对象的一对态射。这些态射对可以用显而易见的方式组成:
Again, a pair of morphisms is just a single morphism in the product category C×D. We define a morphism in a cartesian product of categories as a pair of morphisms which goes from one pair of objects to another pair of objects. These pairs of morphisms can be composed in the obvious way:
(f, g) ∘ (f', g') = (f ∘ f', g ∘ g')(f, g) ∘ (f', g') = (f ∘ f', g ∘ g')
该组合是结合的并且具有恒等性——一对恒等态射(id, id)。所以范畴的笛卡尔积确实是一个范畴。
The composition is associative and it has an identity — a pair of identity morphisms (id, id). So a cartesian product of categories is indeed a category.
但考虑双函子的一种更简单的方法是,它们在两个参数中都是函子。因此,不必将函子定律(结合性和恒等性保留)从函子翻译为双函子,只需针对每个参数单独检查它们就足够了。如果您有从一对范畴到第三个范畴的映射,并且您分别证明它在每个参数中都是函子(即,保持另一个参数不变),那么该映射将自动成为双函子。我所说的函子是指它像诚实函子一样作用于态射。
But an easier way to think about bifunctors is that they are functors in both arguments. So instead of translating functorial laws — associativity and identity preservation — from functors to bifunctors, it’s enough to check them separately for each argument. If you have a mapping from a pair of categories to a third category, and you prove that it is functorial in each argument separately (i.e., keeping the other argument constant), then the mapping is automatically a bifunctor. By functorial I mean that it acts on morphisms like an honest functor.
让我们在 Haskell 中定义一个双函子。在这种情况下,所有三个范畴都是相同的:Haskell 类型的范畴。双函子是一种带有两个类型参数的类型构造函数。Bifunctor这是直接从库中获取的类型类的定义Control.Bifunctor:
Let’s define a bifunctor in Haskell. In this case all three categories are the same: the category of Haskell types. A bifunctor is a type constructor that takes two type arguments. Here’s the definition of the Bifunctor typeclass taken directly from the library Control.Bifunctor:
class Bifunctor f where
bimap :: (a -> c) -> (b -> d) -> f a b -> f c d
bimap g h = first g . second h
first :: (a -> c) -> f a b -> f c b
first g = bimap g id
second :: (b -> d) -> f a b -> f a d
second = bimap idclass Bifunctor f where
bimap :: (a -> c) -> (b -> d) -> f a b -> f c d
bimap g h = first g . second h
first :: (a -> c) -> f a b -> f c b
first g = bimap g id
second :: (b -> d) -> f a b -> f a d
second = bimap id
类型变量f表示双函子。您可以看到,在所有类型签名中,它始终应用于两个类型参数。第一个类型签名定义bimap:同时两个函数的映射。结果是一个提升函数 ,(f a b -> f c d)对 bifunctor 的类型构造函数生成的类型进行操作。在和bimap方面有一个默认实现,这表明每个参数中单独具有函子性就足以定义双函子。firstsecond
The type variable f represents the bifunctor. You can see that in all type signatures it’s always applied to two type arguments. The first type signature defines bimap: a mapping of two functions at once. The result is a lifted function, (f a b -> f c d), operating on types generated by the bifunctor’s type constructor. There is a default implementation of bimap in terms of first and second, which shows that it’s enough to have functoriality in each argument separately to be able to define a bifunctor.
双图
bimap
另外两个类型签名first和是分别见证第一个和第二个参数中的函数性的second两个s。fmapf
The two other type signatures, first and second, are the two fmaps witnessing the functoriality of f in the first and the second argument, respectively.
类型类定义为它们提供了默认实现bimap。
The typeclass definition provides default implementations for both of them in terms of bimap.
声明 的实例时Bifunctor,您可以选择实现并接受和bimap的默认值,或者同时实现和并接受默认值(当然,您可以实现所有三个,但随后由您来确保它们以这种方式相互关联)。firstsecondfirstsecondbimap
When declaring an instance of Bifunctor, you have a choice of either implementing bimap and accepting the defaults for first and second, or implementing both first and second and accepting the default for bimap (of course, you may implement all three of them, but then it’s up to you to make sure they are related to each other in this manner).
双函子的一个重要例子是分类乘积——由通用构造定义的两个对象的乘积。如果任何一对对象都存在乘积,则从这些对象到乘积的映射是双函数的。这在一般情况下是正确的,在 Haskell 中尤其如此。这是Bifunctor配对构造函数的实例 - 最简单的产品类型:
An important example of a bifunctor is the categorical product — a product of two objects that is defined by a universal construction. If the product exists for any pair of objects, the mapping from those objects to the product is bifunctorial. This is true in general, and in Haskell in particular. Here’s the Bifunctor instance for a pair constructor — the simplest product type:
instance Bifunctor (,) where
bimap f g (x, y) = (f x, g y)instance Bifunctor (,) where
bimap f g (x, y) = (f x, g y)
没有太多选择:bimap只需将第一个函数应用于第一个组件,将第二个函数应用于一对中的第二个组件。考虑到类型,代码几乎是自己编写的:
There isn’t much choice: bimap simply applies the first function to the first component, and the second function to the second component of a pair. The code pretty much writes itself, given the types:
bimap :: (a -> c) -> (b -> d) -> (a, b) -> (c, d)bimap :: (a -> c) -> (b -> d) -> (a, b) -> (c, d)
这里双函子的作用是生成类型对,例如:
The action of the bifunctor here is to make pairs of types, for instance:
(,) a b = (a, b)(,) a b = (a, b)
根据对偶性,如果一个余积是为一个范畴中的每对对象定义的,那么它也是一个双函子。在 Haskell 中,这是通过Either类型构造函数作为 的实例来举例说明的Bifunctor:
By duality, a coproduct, if it’s defined for every pair of objects in a category, is also a bifunctor. In Haskell, this is exemplified by the Either type constructor being an instance of Bifunctor:
instance Bifunctor Either where
bimap f _ (Left x) = Left (f x)
bimap _ g (Right y) = Right (g y)instance Bifunctor Either where
bimap f _ (Left x) = Left (f x)
bimap _ g (Right y) = Right (g y)
这段代码也是自己写的。
This code also writes itself.
现在,还记得我们讨论过幺半群范畴吗?幺半群范畴定义了作用于对象的二元运算符以及单位对象。我提到过这Set是一个关于笛卡尔积的幺半群范畴,以单例为一个单位。它也是一个关于不相交并的幺半群范畴,以空集为单位。我没有提到的是,幺半群范畴的要求之一是二元运算符是双函子。这是一个非常重要的要求——我们希望幺半群积与由态射定义的范畴结构兼容。我们现在距离幺半群范畴的完整定义又近了一步(在我们到达那里之前,我们仍然需要了解自然性)。
Now, remember when we talked about monoidal categories? A monoidal category defines a binary operator acting on objects, together with a unit object. I mentioned that Set is a monoidal category with respect to cartesian product, with the singleton set as a unit. And it’s also a monoidal category with respect to disjoint union, with the empty set as a unit. What I haven’t mentioned is that one of the requirements for a monoidal category is that the binary operator be a bifunctor. This is a very important requirement — we want the monoidal product to be compatible with the structure of the category, which is defined by morphisms. We are now one step closer to the full definition of a monoidal category (we still need to learn about naturality, before we can get there).
我们已经看到了几个参数化数据类型的示例,这些数据类型最终都是函子——我们能够fmap为它们定义。复杂的数据类型是由简单的数据类型构造而成的。特别是,代数数据类型 (ADT) 是使用和与积创建的。我们刚刚看到和和乘积是函数式的。我们还知道函子可以组合。因此,如果我们能够证明 ADT 的基本构建块是函数式的,那么我们就会知道参数化 ADT 也是函数式的。
We’ve seen several examples of parameterized data types that turned out to be functors — we were able to define fmap for them. Complex data types are constructed from simpler data types. In particular, algebraic data types (ADTs) are created using sums and products. We have just seen that sums and products are functorial. We also know that functors compose. So if we can show that the basic building blocks of ADTs are functorial, we’ll know that parameterized ADTs are functorial too.
那么参数化代数数据类型的构建块是什么?首先,有些项不依赖于函子的类型参数,例如NothinginMaybe或Nilin List。它们相当于Const函子。请记住,Const函子忽略其类型参数(实际上,第二个类型参数,这是我们感兴趣的类型参数,第一个类型参数保持不变)。
So what are the building blocks of parameterized algebraic data types? First, there are the items that have no dependency on the type parameter of the functor, like Nothing in Maybe, or Nil in List. They are equivalent to the Const functor. Remember, the Const functor ignores its type parameter (really, the second type parameter, which is the one of interest to us, the first one being kept constant).
然后是简单封装类型参数本身的元素,Just例如Maybe. 它们相当于恒等函子。我之前提到过恒等函子,作为Cat中的恒等态射,但没有给出它在 Haskell 中的定义。这里是:
Then there are the elements that simply encapsulate the type parameter itself, like Just in Maybe. They are equivalent to the identity functor. I mentioned the identity functor previously, as the identity morphism in Cat, but didn’t give its definition in Haskell. Here it is:
data Identity a = Identity adata Identity a = Identity a
instance Functor Identity where
fmap f (Identity x) = Identity (f x)instance Functor Identity where
fmap f (Identity x) = Identity (f x)
您可以将其视为Identity最简单的容器,它始终仅存储一个(不可变)类型的值a。
You can think of Identity as the simplest possible container that always stores just one (immutable) value of type a.
代数数据结构中的其他所有内容都是使用积和和从这两个原语构造的。
Everything else in algebraic data structures is constructed from these two primitives using products and sums.
有了这些新知识,让我们重新审视一下Maybe类型构造函数:
With this new knowledge, let’s have a fresh look at the Maybe type constructor:
data Maybe a = Nothing | Just adata Maybe a = Nothing | Just a
它是两种类型的和,我们现在知道这个和是函子的。第一部分,Nothing可以表示为Const ()作用于a( 的第一个类型参数Const设置为单位 - 稍后我们将看到 的更有趣的用法Const)。第二部分只是恒等函子的不同名称。我们可以将Maybe,直到同构定义为:
It’s a sum of two types, and we now know that the sum is functorial. The first part, Nothing can be represented as a Const () acting on a (the first type parameter of Const is set to unit — later we’ll see more interesting uses of Const). The second part is just a different name for the identity functor. We could have defined Maybe, up to isomorphism, as:
type Maybe a = Either (Const () a) (Identity a)type Maybe a = Either (Const () a) (Identity a)
具有两个函子和的双Maybe函子的组成也是如此。(实际上是一个双函子,但在这里我们总是部分应用它。)EitherConst ()IdentityConst
So Maybe is the composition of the bifunctor Either with two functors, Const () and Identity. (Const is really a bifunctor, but here we always use it partially applied.)
我们已经知道,函子的组合就是一个函子——我们可以很容易地说服自己,双函子也是如此。我们所需要的只是弄清楚双函子与两个函子的组合如何作用于态射。给定两个态射,我们只需用一个函子提升一个态射,用另一个函子提升另一个态射。然后,我们用双函子提升所得到的一对提升的态射。
We’ve already seen that a composition of functors is a functor — we can easily convince ourselves that the same is true of bifunctors. All we need is to figure out how a composition of a bifunctor with two functors works on morphisms. Given two morphisms, we simply lift one with one functor and the other with the other functor. We then lift the resulting pair of lifted morphisms with the bifunctor.
我们可以用 Haskell 来表达这个组合。让我们定义一个由双函子参数化的数据类型bf(它是一个类型变量,是接受两种类型作为参数的类型构造函数)、两个函子fu和gu(每个函子接受一个类型变量的类型构造函数)以及两个常规类型a和b。我们应用fu到a和guto b,然后应用bf到结果的两种类型:
We can express this composition in Haskell. Let’s define a data type that is parameterized by a bifunctor bf (it’s a type variable that is a type constructor that takes two types as arguments), two functors fu and gu (type constructors that take one type variable each), and two regular types a and b. We apply fu to a and gu to b, and then apply bf to the resulting two types:
newtype BiComp bf fu gu a b = BiComp (bf (fu a) (gu b))newtype BiComp bf fu gu a b = BiComp (bf (fu a) (gu b))
这就是对象或类型的组合。请注意,在 Haskell 中,我们如何将类型构造函数应用于类型,就像我们将函数应用于参数一样。语法是一样的。
That’s the composition on objects, or types. Notice how in Haskell we apply type constructors to types, just like we apply functions to arguments. The syntax is the same.
如果您有点迷失,请尝试按此顺序BiComp申请Either、Const ()、Identity、a、 和。b您将恢复我们的基本版本Maybe b(a被忽略)。
If you’re getting a little lost, try applying BiComp to Either, Const (), Identity, a, and b, in this order. You will recover our bare-bone version of Maybe b (a is ignored).
新数据类型是andBiComp中的双函子,但前提是is 本身是 a且且是s。编译器必须知道将有available for的定义以及for和的定义。在 Haskell 中,这在实例声明中表示为前提条件:一组类约束,后跟一个双箭头:abbfBifunctorfuguFunctorbimapbffmapfugu
The new data type BiComp is a bifunctor in a and b, but only if bf is itself a Bifunctor and fu and gu are Functors. The compiler must know that there will be a definition of bimap available for bf, and definitions of fmap for fu and gu. In Haskell, this is expressed as a precondition in the instance declaration: a set of class constraints followed by a double arrow:
instance (Bifunctor bf, Functor fu, Functor gu) =>
Bifunctor (BiComp bf fu gu) where
bimap f1 f2 (BiComp x) = BiComp ((bimap (fmap f1) (fmap f2)) x)instance (Bifunctor bf, Functor fu, Functor gu) =>
Bifunctor (BiComp bf fu gu) where
bimap f1 f2 (BiComp x) = BiComp ((bimap (fmap f1) (fmap f2)) x)
bimapfor的实现BiComp以bimapforbf和两个fmapforfu和 的形式给出gu。每当使用时,编译器都会自动推断所有类型并选择正确的重载函数BiComp。
The implementation of bimap for BiComp is given in terms of bimap for bf and the two fmaps for fu and gu. The compiler automatically infers all the types and picks the correct overloaded functions whenever BiComp is used.
的x定义中的bimap具有类型:
The x in the definition of bimap has the type:
bf (fu a) (gu b)bf (fu a) (gu b)
这是相当拗口的。外层bimap突破外层bf,两人分别fmap往下挖fu和gu。f1如果和的类型f2是:
which is quite a mouthful. The outer bimap breaks through the outer bf layer, and the two fmaps dig under fu and gu, respectively. If the types of f1 and f2 are:
f1 :: a -> a'
f2 :: b -> b'f1 :: a -> a'
f2 :: b -> b'
那么最终结果的类型为bf (fu a') (gu b'):
then the final result is of the type bf (fu a') (gu b'):
bimap (fu a -> fu a') -> (gu b -> gu b')
-> bf (fu a) (gu b) -> bf (fu a') (gu b')bimap (fu a -> fu a') -> (gu b -> gu b')
-> bf (fu a) (gu b) -> bf (fu a') (gu b')
如果您喜欢拼图游戏,这些类型的操作可以为您带来数小时的娱乐。
If you like jigsaw puzzles, these kinds of type manipulations can provide hours of entertainment.
所以事实证明,我们不必证明这Maybe是一个函子——这一事实是根据它被构造为两个函子原语之和的方式得出的。
So it turns out that we didn’t have to prove that Maybe was a functor — this fact followed from the way it was constructed as a sum of two functorial primitives.
敏锐的读者可能会问:如果Functor代数数据类型实例的推导如此机械,难道不能由编译器自动执行吗?确实,它可以,而且确实如此。您需要通过在源文件顶部包含以下行来启用特定的 Haskell 扩展:
A perceptive reader might ask the question: If the derivation of the Functor instance for algebraic data types is so mechanical, can’t it be automated and performed by the compiler? Indeed, it can, and it is. You need to enable a particular Haskell extension by including this line at the top of your source file:
{-# LANGUAGE DeriveFunctor #-}{-# LANGUAGE DeriveFunctor #-}
然后添加deriving Functor到您的数据结构:
and then add deriving Functor to your data structure:
data Maybe a = Nothing | Just a
deriving Functordata Maybe a = Nothing | Just a
deriving Functor
fmap并将为您实施相应的操作。
and the corresponding fmap will be implemented for you.
代数数据结构的规律性使得不仅可以派生出Functor几个其他类型类的实例,包括Eq我之前提到的类型类。还可以选择教编译器派生您自己的类型类的实例,但这更高级一些。但其想法是相同的:您提供基本构建块以及求和和乘积的行为,然后让编译器计算其余部分。
The regularity of algebraic data structures makes it possible to derive instances not only of Functor but of several other type classes, including the Eq type class I mentioned before. There is also the option of teaching the compiler to derive instances of your own typeclasses, but that’s a bit more advanced. The idea though is the same: You provide the behavior for the basic building blocks and sums and products, and let the compiler figure out the rest.
如果您是一名 C++ 程序员,那么就实现函子而言,您显然需要靠自己。但是,您应该能够识别 C++ 中某些类型的代数数据结构。如果将这样的数据结构做成通用模板,你应该能够快速实现fmap它。
If you are a C++ programmer, you obviously are on your own as far as implementing functors goes. However, you should be able to recognize some types of algebraic data structures in C++. If such a data structure is made into a generic template, you should be able to quickly implement fmap for it.
让我们看一下树数据结构,我们在 Haskell 中将其定义为递归求和类型:
Let’s have a look at a tree data structure, which we would define in Haskell as a recursive sum type:
data Tree a = Leaf a | Node (Tree a) (Tree a)
deriving Functordata Tree a = Leaf a | Node (Tree a) (Tree a)
deriving Functor
正如我之前提到的,在 C++ 中实现求和类型的一种方法是通过类层次结构。在面向对象的语言中,将其实现fmap为基类的虚函数Functor,然后在所有子类中重写它是很自然的。不幸的是,这是不可能的,因为它fmap是一个模板,不仅通过它所作用的对象的类型(指针this)来参数化,而且还通过应用于它的函数的返回类型来参数化。虚函数无法在 C++ 中模板化。我们将实现fmap为通用自由函数,并将模式匹配替换为dynamic_cast.
As I mentioned before, one way of implementing sum types in C++ is through class hierarchies. It would be natural, in an object-oriented language, to implement fmap as a virtual function of the base class Functor and then override it in all subclasses. Unfortunately this is impossible because fmap is a template, parameterized not only by the type of the object it’s acting upon (the this pointer) but also by the return type of the function that’s been applied to it. Virtual functions cannot be templatized in C++. We’ll implement fmap as a generic free function, and we’ll replace pattern matching with dynamic_cast.
基类必须定义至少一个虚函数才能支持动态转换,因此我们将析构函数设为虚函数(在任何情况下这都是一个好主意):
The base class must define at least one virtual function in order to support dynamic casting, so we’ll make the destructor virtual (which is a good idea in any case):
template<class T>
struct Tree {
virtual ~Tree() {};
};template<class T>
struct Tree {
virtual ~Tree() {};
};
这Leaf只是一个Identity伪装的函子:
The Leaf is just an Identity functor in disguise:
template<class T>
struct Leaf : public Tree<T> {
T _label;
Leaf(T l) : _label(l) {}
};template<class T>
struct Leaf : public Tree<T> {
T _label;
Leaf(T l) : _label(l) {}
};
这Node是一个产品类型:
The Node is a product type:
template<class T>
struct Node : public Tree<T> {
Tree<T> * _left;
Tree<T> * _right;
Node(Tree<T> * l, Tree<T> * r) : _left(l), _right(r) {}
};template<class T>
struct Node : public Tree<T> {
Tree<T> * _left;
Tree<T> * _right;
Node(Tree<T> * l, Tree<T> * r) : _left(l), _right(r) {}
};
在实现时,fmap我们利用Tree. 该Leaf案例应用Identity的版本fmap,并且该Node案例被视为由Tree函子的两个副本组成的双函子。作为一名 C++ 程序员,您可能不习惯用这些术语分析代码,但这是分类思维的一个很好的练习。
When implementing fmap we take advantage of dynamic dispatching on the type of the Tree. The Leaf case applies the Identity version of fmap, and the Node case is treated like a bifunctor composed with two copies of the Tree functor. As a C++ programmer, you’re probably not used to analyzing code in these terms, but it’s a good exercise in categorical thinking.
template<class A, class B>
Tree<B> * fmap(std::function<B(A)> f, Tree<A> * t)
{
Leaf<A> * pl = dynamic_cast <Leaf<A>*>(t);
if (pl)
return new Leaf<B>(f (pl->_label));
Node<A> * pn = dynamic_cast<Node<A>*>(t);
if (pn)
return new Node<B>( fmap<A>(f, pn->_left)
, fmap<A>(f, pn->_right));
return nullptr;
}template<class A, class B>
Tree<B> * fmap(std::function<B(A)> f, Tree<A> * t)
{
Leaf<A> * pl = dynamic_cast <Leaf<A>*>(t);
if (pl)
return new Leaf<B>(f (pl->_label));
Node<A> * pn = dynamic_cast<Node<A>*>(t);
if (pn)
return new Node<B>( fmap<A>(f, pn->_left)
, fmap<A>(f, pn->_right));
return nullptr;
}
为简单起见,我决定忽略内存和资源管理问题,但在生产代码中,您可能会使用智能指针(唯一或共享,具体取决于您的策略)。
For simplicity, I decided to ignore memory and resource management issues, but in production code you would probably use smart pointers (unique or shared, depending on your policy).
将其与 Haskell 实现进行比较fmap:
Compare it with the Haskell implementation of fmap:
instance Functor Tree where
fmap f (Leaf a) = Leaf (f a)
fmap f (Node t t') = Node (fmap f t) (fmap f t')instance Functor Tree where
fmap f (Leaf a) = Leaf (f a)
fmap f (Node t t') = Node (fmap f t) (fmap f t')
该实现也可以由编译器自动导出。
This implementation can also be automatically derived by the compiler.
我承诺我会回到我之前描述的克莱斯利范畴。该范畴中的态射被表示为返回Writer数据结构的“修饰”函数。
I promised that I would come back to the Kleisli category I described earlier. Morphisms in that category were represented as “embellished” functions returning the Writer data structure.
type Writer a = (a, String)type Writer a = (a, String)
我说过修饰与内函子有某种关系。事实上,Writer类型构造函数在a. 我们甚至不需要实现fmap它,因为它只是一个简单的产品类型。
I said that the embellishment was somehow related to endofunctors. And, indeed, the Writer type constructor is functorial in a. We don’t even have to implement fmap for it, because it’s just a simple product type.
但一般来说,克莱斯利范畴和函子之间的关系是什么?克莱斯利范畴作为一个范畴,定义了组成和同一性。让我提醒您一下,成分是由鱼类操作员给出的:
But what’s the relation between a Kleisli category and a functor — in general? A Kleisli category, being a category, defines composition and identity. Let’ me remind you that the composition is given by the fish operator:
(>=>) :: (a -> Writer b) -> (b -> Writer c) -> (a -> Writer c)
m1 >=> m2 = \x ->
let (y, s1) = m1 x
(z, s2) = m2 y
in (z, s1 ++ s2)(>=>) :: (a -> Writer b) -> (b -> Writer c) -> (a -> Writer c)
m1 >=> m2 = \x ->
let (y, s1) = m1 x
(z, s2) = m2 y
in (z, s1 ++ s2)
以及由称为 的函数进行的恒等态射return:
and the identity morphism by a function called return:
return :: a -> Writer a
return x = (x, "")return :: a -> Writer a
return x = (x, "")
事实证明,如果您足够长地查看这两个函数的类型(我的意思是,足够长),您可以找到一种方法将它们组合起来以生成具有正确类型签名的函数来充当fmap. 像这样:
It turns out that, if you look at the types of these two functions long enough (and I mean, long enough), you can find a way to combine them to produce a function with the right type signature to serve as fmap. Like this:
fmap f = id >=> (\x -> return (f x))fmap f = id >=> (\x -> return (f x))
在这里,fish 运算符组合了两个函数:其中一个是熟悉的id,另一个是 lambda,它应用于对 lambda 参数return进行操作的结果。f最难的部分可能是使用id. Fish 运算符的参数不应该是一个采用“正常”类型并返回修饰类型的函数吗?嗯,不是真的。a没有人说它a -> Writer b一定是“正常”类型。它是一个类型变量,因此它可以是任何东西,特别是它可以是修饰类型,例如Writer b.
Here, the fish operator combines two functions: one of them is the familiar id, and the other is a lambda that applies return to the result of acting with f on the lambda’s argument. The hardest part to wrap your brain around is probably the use of id. Isn’t the argument to the fish operator supposed to be a function that takes a “normal” type and returns an embellished type? Well, not really. Nobody says that a in a -> Writer b must be a “normal” type. It’s a type variable, so it can be anything, in particular it can be an embellished type, like Writer b.
因此id将把Writer a它变成Writer a. Fish 操作符将找出 的值a并将其传递x给 lambda。在那里,f将把它变成 ab并return修饰它,使其成为Writer b。把它们放在一起,我们最终得到一个接受Writer a并返回 的函数Writer b,这正是fmap应该产生的结果。
So id will take Writer a and turn it into Writer a. The fish operator will fish out the value of a and pass it as x to the lambda. There, f will turn it into a b and return will embellish it, making it Writer b. Putting it all together, we end up with a function that takes Writer a and returns Writer b, exactly what fmap is supposed to produce.
请注意,这个参数非常通用:您可以替换Writer为任何类型构造函数。只要它支持 Fish 操作符 和,您也return可以定义。fmap所以 Kleisli 范畴中的修饰始终是函子。(不过,并不是每个函子都会产生克莱斯利范畴。)
Notice that this argument is very general: you can replace Writer with any type constructor. As long as it supports a fish operator and return, you can define fmap as well. So the embellishment in the Kleisli category is always a functor. (Not every functor, though, gives rise to a Kleisli category.)
您可能想知道fmap我们刚刚定义的 是否与fmap编译器为我们派生的相同deriving Functor。有趣的是,确实如此。这是由于 Haskell 实现多态函数的方式造成的。它被称为参数多态性,并且它是所谓的免费定理的来源。fmap这些定理之一说,如果给定类型构造函数有一个保留身份的实现,那么它必须是唯一的。
You might wonder if the fmap we have just defined is the same fmap the compiler would have derived for us with deriving Functor. Interestingly enough, it is. This is due to the way Haskell implements polymorphic functions. It’s called parametric polymorphism, and it’s a source of so called theorems for free. One of those theorems says that, if there is an implementation of fmap for a given type constructor, one that preserves identity, then it must be unique.
现在我们已经回顾了 writer 函子,让我们回到 reader 函子。它基于部分应用的函数箭头类型构造函数:
Now that we’ve reviewed the writer functor, let’s go back to the reader functor. It was based on the partially applied function-arrow type constructor:
(->) r(->) r
我们可以将其重写为类型同义词:
We can rewrite it as a type synonym:
type Reader r a = r -> atype Reader r a = r -> a
Functor正如我们之前看到的,该实例如下:
for which the Functor instance, as we’ve seen before, reads:
instance Functor (Reader r) where
fmap f g = f . ginstance Functor (Reader r) where
fmap f g = f . g
但就像pair类型构造函数或Either类型构造函数一样,函数类型构造函数也接受两个类型参数。这对 和Either在两个论证中都是函子——它们是双函子。函数构造函数也是双函子吗?
But just like the pair type constructor, or the Either type constructor, the function type constructor takes two type arguments. The pair and Either were functorial in both arguments — they were bifunctors. Is the function constructor a bifunctor too?
让我们尝试在第一个参数中使其成为函数式的。我们将从类型同义词开始——它就像Reader但参数翻转了:
Let’s try to make it functorial in the first argument. We’ll start with a type synonym — it’s just like the Reader but with the arguments flipped:
type Op r a = a -> rtype Op r a = a -> r
这次我们修复了返回类型 ,r并改变了参数类型a。让我们看看是否可以以某种方式匹配类型来实现fmap,它将具有以下类型签名:
This time we fix the return type, r, and vary the argument type, a. Let’s see if we can somehow match the types in order to implement fmap, which would have the following type signature:
fmap :: (a -> b) -> (a -> r) -> (b -> r)fmap :: (a -> b) -> (a -> r) -> (b -> r)
只有两个函数a分别接受和返回b和r,根本没有办法构建一个函数接受b和返回r!如果我们能以某种方式反转第一个函数,使其接受b并返回,情况就会有所不同a。我们不能反转任意函数,但我们可以转到相反的范畴。
With just two functions taking a and returning, respectively, b and r, there is simply no way to build a function taking b and returning r! It would be different if we could somehow invert the first function, so that it took b and returned a instead. We can’t invert an arbitrary function, but we can go to the opposite category.
简短回顾:对于每个范畴C都有一个双重范畴C op。它是一个与C具有相同对象的范畴,但所有箭头都相反。
A short recap: For every category C there is a dual category Cop. It’s a category with the same objects as C, but with all the arrows reversed.
考虑一个介于C op和其他范畴D之间的函子:
F :: C op → D
这样的函子将C op中的态射f op :: a → b映射到态射F f op :: F a → F b在D . 但态射f op秘密地对应于原始范畴C中的某个态射f :: b → a。注意反转。
Consider a functor that goes between Cop and some other category D:
F :: Cop → D
Such a functor maps a morphism fop :: a → b in Cop to the morphism F fop :: F a → F b in D. But the morphism fop secretly corresponds to some morphism f :: b → a in the original category C. Notice the inversion.
现在,F是一个常规函子,但我们可以基于F定义另一个映射,它不是函子 - 我们称之为G。这是从C到D的映射。它以与F相同的方式映射对象,但当涉及到映射态射时,它会反转它们。它采用C中的态射f :: b → a,首先将其映射到相反的态射f op :: a → b,然后在其上使用函子 F ,以获得F f op :: F a → F b。
Now, F is a regular functor, but there is another mapping we can define based on F, which is not a functor — let’s call it G. It’s a mapping from C to D. It maps objects the same way F does, but when it comes to mapping morphisms, it reverses them. It takes a morphism f :: b → a in C, maps it first to the opposite morphism fop :: a → b and then uses the functor F on it, to get F fop :: F a → F b.
考虑到F a与 Ga相同,F b与G b相同,整个过程可以描述为:
G f :: (b → a) → (G a → G b)
这是一个“函子一个转折。” 以这种方式反转态射方向的范畴映射称为逆变函子。请注意,逆变函子只是来自相反范畴的常规函子。顺便说一句,常规函子(我们迄今为止一直在研究的那种)称为协变函子。
Considering that F a is the same as G a and F b is the same as G b, the whole trip can be described as:
G f :: (b → a) → (G a → G b)
It’s a “functor with a twist.” A mapping of categories that inverts the direction of morphisms in this manner is called a contravariant functor. Notice that a contravariant functor is just a regular functor from the opposite category. The regular functors, by the way — the kind we’ve been studying thus far — are called covariant functors.
这是在 Haskell 中定义逆变函子(实际上是逆变内函子)的类型类:
Here’s the typeclass defining a contravariant functor (really, a contravariant endofunctor) in Haskell:
class Contravariant f where
contramap :: (b -> a) -> (f a -> f b)class Contravariant f where
contramap :: (b -> a) -> (f a -> f b)
我们的类型构造函数Op是它的一个实例:
Our type constructor Op is an instance of it:
instance Contravariant (Op r) where
-- (b -> a) -> Op r a -> Op r b
contramap f g = g . finstance Contravariant (Op r) where
-- (b -> a) -> Op r a -> Op r b
contramap f g = g . f
请注意,该函数f将自身插入到— 函数的内容之前(即右侧)。Opg
Notice that the function f inserts itself before (that is, to the right of) the contents of Op — the function g.
如果您注意到 for 只是参数翻转的函数组合运算符,那么 for 的定义可能会更加contramap简洁。Op有一个用于翻转参数的特殊函数,称为flip:
The definition of contramap for Op may be made even terser, if you notice that it’s just the function composition operator with the arguments flipped. There is a special function for flipping arguments, called flip:
flip :: (a -> b -> c) -> (b -> a -> c)
flip f y x = f x yflip :: (a -> b -> c) -> (b -> a -> c)
flip f y x = f x y
有了它,我们得到:
With it, we get:
contramap = flip (.)contramap = flip (.)
我们已经看到,函数箭头运算符的第一个参数是逆变的,第二个参数是协变的。这样的野兽有名字吗?事实证明,如果目标范畴是Set,那么这样的野兽就被称为profunctor。因为逆变函子等价于相反范畴的协变函子,所以泛函子定义为:
C op × D → Set
We’ve seen that the function-arrow operator is contravariant in its first argument and covariant in the second. Is there a name for such a beast? It turns out that, if the target category is Set, such a beast is called a profunctor. Because a contravariant functor is equivalent to a covariant functor from the opposite category, a profunctor is defined as:
Cop × D → Set
由于从第一个近似来看,Haskell 类型是集合,因此我们将名称应用于两个参数的Profunctor类型构造函数p,这在第一个参数中是反函子,在第二个参数中是函子。这是从库中获取的适当类型类Data.Profunctor:
Since, to first approximation, Haskell types are sets, we apply the name Profunctor to a type constructor p of two arguments, which is contra-functorial in the first argument and functorial in the second. Here’s the appropriate typeclass taken from the Data.Profunctor library:
class Profunctor p where
dimap :: (a -> b) -> (c -> d) -> p b c -> p a d
dimap f g = lmap f . rmap g
lmap :: (a -> b) -> p b c -> p a c
lmap f = dimap f id
rmap :: (b -> c) -> p a b -> p a c
rmap = dimap idclass Profunctor p where
dimap :: (a -> b) -> (c -> d) -> p b c -> p a d
dimap f g = lmap f . rmap g
lmap :: (a -> b) -> p b c -> p a c
lmap f = dimap f id
rmap :: (b -> c) -> p a b -> p a c
rmap = dimap id
所有三个函数都有默认实现。就像 with 一样Bifunctor,在声明 的实例时Profunctor,您可以选择实现并接受anddimap的默认值,或者同时实现and并接受默认值。lmaprmaplmaprmapdimap
All three functions come with default implementations. Just like with Bifunctor, when declaring an instance of Profunctor, you have a choice of either implementing dimap and accepting the defaults for lmap and rmap, or implementing both lmap and rmap and accepting the default for dimap.
迪马普
dimap
现在我们可以断言函数箭头运算符是 a 的实例Profunctor:
Now we can assert that the function-arrow operator is an instance of a Profunctor:
instance Profunctor (->) where
dimap ab cd bc = cd . bc . ab
lmap = flip (.)
rmap = (.)instance Profunctor (->) where
dimap ab cd bc = cd . bc . ab
lmap = flip (.)
rmap = (.)
Profunctors 在 Haskell 透镜库中有其应用。当我们谈论结局和共结局时,我们会再次见到它们。
Profunctors have their application in the Haskell lens library. We’ll see them again when we talk about ends and coends.
显示数据类型:
data Pair a b = Pair a b
是一个双函子。对于额外的信用,实施所有三种方法Bifunctor并使用等式推理来表明这些定义在可以应用时与默认实现兼容。
Show that the data type:
data Pair a b = Pair a b
is a bifunctor. For additional credit implement all three methods of Bifunctor and use equational reasoning to show that these definitions are compatible with the default implementations whenever they can be applied.
显示 的标准定义Maybe和此脱糖之间的同构:
type Maybe' a = Either (Const () a) (Identity a)
提示:在两个实现之间定义两个映射。为了获得额外的分数,请使用等式推理证明它们是彼此相反的。
Show the isomorphism between the standard definition of Maybe and this desugaring:
type Maybe' a = Either (Const () a) (Identity a)
Hint: Define two mappings between the two implementations. For additional credit, show that they are the inverse of each other using equational reasoning.
让我们尝试另一种数据结构。我称其为 aPreList因为它是 a 的先驱List。它用类型参数代替递归b。
data PreList a b = Nil | Cons a b
List您可以通过递归地应用于PreList自身来恢复我们之前对 a 的定义(当我们讨论不动点时,我们将看到它是如何完成的)。
显示PreList是 的一个实例Bifunctor。
Let’s try another data structure. I call it a PreList because it’s a precursor to a List. It replaces recursion with a type parameter b.
data PreList a b = Nil | Cons a b
You could recover our earlier definition of a List by recursively applying PreList to itself (we’ll see how it’s done when we talk about fixed points).
Show that PreList is an instance of Bifunctor.
证明以下数据类型定义了a和中的双函子b:
data K2 c a b = K2 c
data Fst a b = Fst a
data Snd a b = Snd b
Show that the following data types define bifunctors in a and b:
data K2 c a b = K2 c
data Fst a b = Fst a
data Snd a b = Snd b
For additional credit, check your solutions agains Conor McBride’s paper Clowns to the Left of me, Jokers to the Right.
bimap该语言中的通用对。bimap for a generic pair in that language.std::map被视为两个模板参数Key和中的双函子或函子T吗?您将如何重新设计这种数据类型以实现这一目标?std::map be considered a bifunctor or a profunctor in the two template arguments Key and T? How would you redesign this data type to make it so?与往常一样,非常感谢 Gershom Bazerman 审阅本文。
As usual, big thanks go to Gershom Bazerman for reviewing this article.
到目前为止,我一直在掩盖函数类型的含义。函数类型不同于其他类型。
So far I’ve been glossing over the meaning of function types. A function type is different from other types.
以Integer为例:它只是一组整数。Bool是一个二元集。但函数类型a->b不仅如此:它是对象a和之间的一组态射b。任何范畴中两个对象之间的态射集称为 hom 集。碰巧的是,在集合范畴中,每个hom-set 本身就是同一范畴中的一个对象——因为它毕竟是一个集合。
Take Integer, for instance: It’s just a set of integers. Bool is a two element set. But a function type a->b is more than that: it’s a set of morphisms between objects a and b. A set of morphisms between two objects in any category is called a hom-set. It just so happens that in the category Set every hom-set is itself an object in the same category —because it is, after all, a set.
Set中的Hom-set只是一个集合
Hom-set in Set is just a set
对于其他范畴而言,情况并非如此,其中 hom-sets 位于范畴之外。它们甚至被称为外部主机。
The same is not true of other categories where hom-sets are external to a category. They are even called external hom-sets.
C 类中的 Hom-set 是外部集
Hom-set in category C is an external set
正是范畴Set的自引用性质使函数类型变得特殊。但至少在某些范畴中,有一种方法可以构造表示 hom 集的对象。此类对象称为内部hom-set。
It’s the self-referential nature of the category Set that makes function types special. But there is a way, at least in some categories, to construct objects that represent hom-sets. Such objects are called internal hom-sets.
让我们暂时忘记函数类型是集合,并尝试从头开始构造一个函数类型,或更一般地说,一个内部 hom 集。像往常一样,我们将从“集合”范畴中获取线索,但要小心避免使用集合的任何属性,以便该构造将自动适用于其他范畴。
Let’s forget for a moment that function types are sets and try to construct a function type, or more generally, an internal hom-set, from scratch. As usual, we’ll take our cues from the Set category, but carefully avoid using any properties of sets, so that the construction will automatically work for other categories.
由于函数类型与参数类型和结果类型的关系,因此可以将其视为复合类型。我们已经了解了复合类型的构造——那些涉及对象之间关系的构造。我们使用通用结构来定义产品类型和联产品类型。我们可以使用相同的技巧来定义函数类型。我们需要一个涉及三个对象的模式:我们正在构造的函数类型、参数类型和结果类型。
A function type may be considered a composite type because of its relationship to the argument type and the result type. We’ve already seen the constructions of composite types — those that involved relationships between objects. We used universal constructions to define a product type and a coproduct types. We can use the same trick to define a function type. We will need a pattern that involves three objects: the function type that we are constructing, the argument type, and the result type.
连接这三种类型的明显模式称为函数应用或求值。给定一个函数类型的候选者,让我们调用它z(注意,如果我们不在范畴Set中,这只是一个像任何其他对象一样的对象),以及参数类型a(一个对象),应用程序将此对映射到结果类型b(对象)。我们有三个对象,其中两个是固定的(代表参数类型和结果类型的)。
The obvious pattern that connects these three types is called function application or evaluation. Given a candidate for a function type, let’s call it z (notice that, if we are not in the category Set, this is just an object like any other object), and the argument type a (an object), the application maps this pair to the result type b (an object). We have three objects, two of them fixed (the ones representing the argument type and the result type).
我们还有应用程序,它是一个映射。我们如何将这种映射合并到我们的模式中?如果我们被允许查看对象内部,我们可以将一个函数f( 的一个元素z)与一个参数x( 的一个元素a)配对,并将其映射到( tof x的应用程序,它是 的一个元素)。fxb
We also have the application, which is a mapping. How do we incorporate this mapping into our pattern? If we were allowed to look inside objects, we could pair a function f (an element of z) with an argument x (an element of a) and map it to f x (the application of f to x, which is an element of b).
在 Set 中,我们可以从一组函数 z 中选择一个函数 f,并且可以从集合(类型)a 中选择一个参数 x。我们得到集合(类型)b 中的一个元素 fx。
In Set we can pick a function f from a set of functions z and we can pick an argument x from the set (type) a. We get an element f x in the set (type) b.
但我们不只处理单个对(f, x),还可以讨论函数类型和参数类型的整个乘积。产品是一个对象,我们可以选择从该对象到 的箭头作为我们的应用程序态射。在Set中,是将每对映射到的函数。zaz×agbg(f, x)f x
But instead of dealing with individual pairs (f, x), we can as well talk about the whole product of the function type z and the argument type a. The product z×a is an object, and we can pick, as our application morphism, an arrow g from that object to b. In Set, g would be the function that maps every pair (f, x) to f x.
这就是模式:两个对象的乘积z并通过态射a连接到另一个对象。bg
So that’s the pattern: a product of two objects z and a connected to another object b by a morphism g.
对象和态射的模式,是通用构造的起点
A pattern of objects and morphisms that is the starting point of the universal construction
该模式是否足够具体,可以使用通用结构来挑选出函数类型?并非在每个范畴中。但在我们感兴趣的范畴中确实如此。还有一个问题:是否可以在不先定义产品的情况下定义函数对象?有些范畴中没有产品,或者没有适用于所有对象对的产品。答案是否定的:如果没有产品类型,就没有功能类型。稍后当我们讨论指数时我们会再讨论这一点。
Is this pattern specific enough to single out the function type using a universal construction? Not in every category. But in the categories of interest to us it is. And another question: Would it be possible to define a function object without first defining a product? There are categories in which there is no product, or there isn’t a product for all pairs of objects. The answer is no: there is no function type, if there is no product type. We’ll come back to this later when we talk about exponentials.
让我们回顾一下通用构造。我们从对象和态射的模式开始。这是我们不精确的查询,它通常会产生大量的命中。特别是,在Set中,几乎所有事物都相互关联。我们可以采用任何对象z,形成其与 的乘积a,并且将有一个从它 到 的函数b(除非b是空集)。
Let’s review the universal construction. We start with a pattern of objects and morphisms. That’s our imprecise query, and it usually yields lots and lots of hits. In particular, in Set, pretty much everything is connected to everything. We can take any object z, form its product with a, and there’s going to be a function from it to b (except when b is an empty set).
这就是我们应用秘密武器的时候:排名。这通常是通过要求候选对象之间存在唯一的映射来完成的——这种映射以某种方式分解了我们的构造。在我们的例子中,我们将判定,当且仅当存在从到的唯一z映射,使得通过应用 来应用因子时,与到的态射一起优于具有其自身应用的其他态射。(提示:边看图边读这句话。)gz×abz'g'hz'zg'g
That’s when we apply our secret weapon: ranking. This is usually done by requiring that there be a unique mapping between candidate objects — a mapping that somehow factorizes our construction. In our case, we’ll decree that z together with the morphism g from z×a to b is better than some other z' with its own application g', if and only if there is a unique mapping h from z' to z such that the application of g' factors through the application of g. (Hint: Read this sentence while looking at the picture.)
在函数对象的候选者之间建立排名
Establishing a ranking between candidates for the function object
现在这是棘手的部分,也是我推迟这个特殊的通用构造至今的主要原因。给定态射,我们想要闭合同时具有和 与交叉的h :: z'-> z图。考虑到从到 的映射,我们真正需要的是从到 的映射。现在,在讨论了产品的功能性之后,我们知道了如何去做。因为乘积本身是一个函子(更准确地说是一个内双函子),所以可以提升态射对。换句话说,我们不仅可以定义对象的乘积,还可以定义态射的乘积。z'zahz'zz'×az×a
Now here’s the tricky part, and the main reason I postponed this particular universal construction till now. Given the morphism h :: z'-> z, we want to close the diagram that has both z' and z crossed with a. What we really need, given the mapping h from z' to z, is a mapping from z'×a to z×a. And now, after discussing the functoriality of the product, we know how to do it. Because the product itself is a functor (more precisely an endo-bi-functor), it’s possible to lift pairs of morphisms. In other words, we can define not only products of objects but also products of morphisms.
由于我们没有触及乘积的第二个组成部分z'×a,因此我们将提升一对态射(h, id),其中id是 上的恒等式a。
Since we are not touching the second component of the product z'×a, we will lift the pair of morphisms (h, id), where id is an identity on a.
因此,我们可以通过以下方式将一个应用程序g从另一个应用程序中分解出来g':
So, here’s how we can factor one application, g, out of another application g':
g' = g ∘ (h × id)g' = g ∘ (h × id)
这里的关键是乘积对态射的作用。
The key here is the action of the product on morphisms.
普遍构造的第三部分是选择普遍最好的对象。让我们调用这个对象a⇒b(将其视为一个对象的符号名称,不要与 Haskell 类型类约束混淆 - 我稍后将讨论命名它的不同方式)。这个对象有它自己的应用程序——从(a⇒b)×ato 的态射b——我们称之为eval。a⇒b如果函数对象的任何其他候选对象可以唯一地映射到它,从而使其应用态射g因式分解,则该对象是最好的eval。根据我们的排名,该对象比任何其他对象都要好。
The third part of the universal construction is selecting the object that is universally the best. Let’s call this object a⇒b (think of this as a symbolic name for one object, not to be confused with a Haskell typeclass constraint — I’ll discuss different ways of naming it later). This object comes with its own application — a morphism from (a⇒b)×a to b — which we will call eval. The object a⇒b is the best if any other candidate for a function object can be uniquely mapped to it in such a way that its application morphism g factorizes through eval. This object is better than any other object according to our ranking.
通用函数对象的定义。这与上面的图相同,但现在对象a⇒b是通用的。
The definition of the universal function object. This is the same diagram as above, but now the object a⇒b is universal.
正式:
Formally:
从到 的函数对象是与态射一起的
对象aba⇒b
这样对于任何其他 such that for any other object
存在唯一的态射 there is a unique morphism
that factors |
当然,不能保证a⇒b任何一对对象a和b给定范畴中都存在这样的对象。但在Set中总是如此。此外,在Set中,该对象与 hom-set Set(a, b)同构。
Of course, there is no guarantee that such an object a⇒b exists for any pair of objects a and b in a given category. But it always does in Set. Moreover, in Set, this object is isomorphic to the hom-set Set(a, b).
这就是为什么在 Haskell 中,我们将函数类型解释a->b为分类函数对象a⇒b。
This is why, in Haskell, we interpret the function type a->b as the categorical function object a⇒b.
让我们再看一下函数对象的所有候选对象。然而,这一次,让我们将态射视为两个变量和 的g函数。za
Let’s have a second look at all the candidates for the function object. This time, however, let’s think of the morphism g as a function of two variables, z and a.
g :: z × a -> bg :: z × a -> b
作为乘积的态射就很接近于作为两个变量的函数。特别是,在Set中,g是来自一对值的函数,一个来自 set z,一个来自 set a。
Being a morphism from a product comes as close as it gets to being a function of two variables. In particular, in Set, g is a function from pairs of values, one from the set z and one from the set a.
另一方面,通用属性告诉我们,对于每个这样的态射,g都有一个h映射z到函数对象的唯一态射a⇒b。
On the other hand, the universal property tells us that for each such g there is a unique morphism h that maps z to a function object a⇒b.
h :: z -> (a⇒b)h :: z -> (a⇒b)
在Set中,这仅意味着h是一个函数,它接受一个类型变量z并从ato返回一个函数b。这就形成了h一个高阶函数。因此,通用构造在两个变量的函数和一个变量返回函数的函数之间建立了一一对应关系。这种对应关系称为柯里化,并h称为 的柯里化版本g。
In Set, this just means that h is a function that takes one variable of type z and returns a function from a to b. That makes h a higher order function. Therefore the universal construction establishes a one-to-one correspondence between functions of two variables and functions of one variable returning functions. This correspondence is called currying, and h is called the curried version of g.
这种对应关系是一对一的,因为给定任何一个g都有一个唯一的h,并且给定任何你总是可以使用以下公式h重新创建两个参数函数:g
This correspondence is one-to-one, because given any g there is a unique h, and given any h you can always recreate the two-argument function g using the formula:
g = eval ∘ (h × id)g = eval ∘ (h × id)
该函数g可以称为 的未柯里化版本h。
The function g can be called the uncurried version of h.
柯里化本质上是内置于 Haskell 语法中的。一个函数返回一个函数:
Currying is essentially built into the syntax of Haskell. A function returning a function:
a -> (b -> c)a -> (b -> c)
通常被认为是两个变量的函数。这就是我们如何阅读不带括号的签名:
is often thought of as a function of two variables. That’s how we read the un-parenthesized signature:
a -> b -> ca -> b -> c
这种解释在我们定义多参数函数的方式中很明显。例如:
This interpretation is apparent in the way we define multi-argument functions. For instance:
catstr :: String -> String -> String
catstr s s’ = s ++ s’catstr :: String -> String -> String
catstr s s’ = s ++ s’
相同的函数可以写成一个返回一个函数的单参数函数——一个 lambda:
The same function can be written as a one-argument function returning a function — a lambda:
catstr’ s = \s’ -> s ++ s’catstr’ s = \s’ -> s ++ s’
这两个定义是等价的,并且任一定义都可以部分应用于一个参数,生成一个单参数函数,如下所示:
These two definitions are equivalent, and either can be partially applied to just one argument, producing a one-argument function, as in:
greet :: String -> String
greet = catstr “Hello “greet :: String -> String
greet = catstr “Hello “
严格来说,两个变量的函数是采用一对(产品类型)的函数:
Strictly speaking, a function of two variables is one that takes a pair (a product type):
(a, b) -> c(a, b) -> c
在两种表示形式之间进行转换很简单,毫不奇怪,执行此操作的两个(高阶)函数被称为curry和uncurry:
It’s trivial to convert between the two representations, and the two (higher-order) functions that do it are called, unsurprisingly, curry and uncurry:
curry :: ((a, b)->c) -> (a->b->c)
curry f a b = f (a, b)curry :: ((a, b)->c) -> (a->b->c)
curry f a b = f (a, b)
和
and
uncurry :: (a->b->c) -> ((a, b)->c)
uncurry f (a, b) = f a buncurry :: (a->b->c) -> ((a, b)->c)
uncurry f (a, b) = f a b
请注意,这curry是用于函数对象的通用构造的因式分解器。如果以这种形式重写,这一点尤其明显:
Notice that curry is the factorizer for the universal construction of the function object. This is especially apparent if it’s rewritten in this form:
factorizer :: ((a, b)->c) -> (a->(b->c))
factorizer g = \a -> (\b -> g (a, b))factorizer :: ((a, b)->c) -> (a->(b->c))
factorizer g = \a -> (\b -> g (a, b))
(提醒一下:因式分解器从候选项生成因式分解函数。)
(As a reminder: A factorizer produces the factorizing function from a candidate.)
在非函数式语言(如 C++)中,柯里化是可能的,但并不简单。您可以将 C++ 中的多参数函数视为对应于采用元组的 Haskell 函数(不过,更令人困惑的是,在 C++ 中,您可以定义采用显式 的函数,以及可变参数函数和采用初始值设定项列表的函数)std::tuple。
In non-functional languages, like C++, currying is possible but nontrivial. You can think of multi-argument functions in C++ as corresponding to Haskell functions taking tuples (although, to confuse things even more, in C++ you can define functions that take an explicit std::tuple, as well as variadic functions, and functions taking initializer lists).
您可以使用模板部分应用 C++ 函数std::bind。例如,给定两个字符串的函数:
You can partially apply a C++ function using the template std::bind. For instance, given a function of two strings:
std::string catstr(std::string s1, std::string s2) {
return s1 + s2;
}std::string catstr(std::string s1, std::string s2) {
return s1 + s2;
}
您可以定义一个字符串的函数:
you can define a function of one string:
using namespace std::placeholders;
auto greet = std::bind(catstr, "Hello ", _1);
std::cout << greet("Haskell Curry");using namespace std::placeholders;
auto greet = std::bind(catstr, "Hello ", _1);
std::cout << greet("Haskell Curry");
Scala 比 C++ 或 Java 更实用,介于两者之间。如果您预计您定义的函数将被部分应用,您可以使用多个参数列表来定义它:
Scala, which is more functional than C++ or Java, falls somewhere in between. If you anticipate that the function you’re defining will be partially applied, you define it with multiple argument lists:
def catstr(s1: String)(s2: String) = s1 + s2def catstr(s1: String)(s2: String) = s1 + s2
当然,这需要图书馆作者具有一定的远见或先见之明。
Of course that requires some amount of foresight or prescience on the part of a library writer.
a在数学文献中,函数对象,或者两个对象和之间的内部 hom 对象b,通常称为指数并用 表示ba。请注意,参数类型位于指数中。这种表示法一开始可能看起来很奇怪,但如果你考虑一下功能和产品之间的关系,它就很有意义了。我们已经看到,我们必须在内部 hom 对象的通用构造中使用乘积,但联系比这更深。
In mathematical literature, the function object, or the internal hom-object between two objects a and b, is often called the exponential and denoted by ba. Notice that the argument type is in the exponent. This notation might seem strange at first, but it makes perfect sense if you think of the relationship between functions and products. We’ve already seen that we have to use the product in the universal construction of the internal hom-object, but the connection goes deeper than that.
当您考虑有限类型(具有有限数量的值的类型,例如Bool、Char、甚至Int或)之间的函数时,这一点最为明显Double。这些函数至少在原则上可以完全记忆或转化为可供查找的数据结构。这就是函数(即态射)与函数类型(即对象)之间等价的本质。
This is best seen when you consider functions between finite types — types that have a finite number of values, like Bool, Char, or even Int or Double. Such functions, at least in principle, can be fully memoized or turned into data structures to be looked up. And this is the essence of the equivalence between functions, which are morphisms, and function types, which are objects.
例如,(纯)函数 fromBool完全由一对值指定:一个对应于False,另一个对应于True。Bool比如说,从 到 的所有可能函数的集合是所有sInt对的集合。Int这与产品相同Int × Int,或者,在符号上有点创意,Int2.
For instance a (pure) function from Bool is completely specified by a pair of values: one corresponding to False, and one corresponding to True. The set of all possible functions from Bool to, say, Int is the set of all pairs of Ints. This is the same as the product Int × Int or, being a little creative with notation, Int2.
再举个例子,让我们看看 C++ 类型char,它包含 256 个值(HaskellChar更大,因为 Haskell 使用 Unicode)。C++ 标准库中有几个函数通常是使用查找来实现的。isupper类似或 的函数isspace是使用表实现的,相当于 256 个布尔值的元组。元组是一种乘积类型,因此我们正在处理 256 个布尔值的乘积:bool × bool × bool × ... × bool。我们从算术中知道,迭代乘积定义了幂。如果你“乘”bool自身 256(或char)次,你就得到, 或 的bool幂。charboolchar
For another example, let’s look at the C++ type char, which contains 256 values (Haskell Char is larger, because Haskell uses Unicode). There are several functions in the part of the C++ Standard Library that are usually implemented using lookups. Functions like isupper or isspace are implemented using tables, which are equivalent to tuples of 256 Boolean values. A tuple is a product type, so we are dealing with products of 256 Booleans: bool × bool × bool × ... × bool. We know from arithmetics that an iterated product defines a power. If you “multiply” bool by itself 256 (or char) times, you get bool to the power of char, or boolchar.
定义为 256 元组的类型中有多少个值bool?正好 2 256。char这也是从到的不同函数的数量bool,每个函数对应一个唯一的 256 元组。bool您可以类似地计算出从到的函数数量char为 256 2,依此类推。在这些情况下,函数类型的指数表示法非常有意义。
How many values are there in the type defined as 256-tuples of bool? Exactly 2256. This is also the number of different functions from char to bool, each function corresponding to a unique 256-tuple. You can similarly calculate that the number of functions from bool to char is 2562, and so on. The exponential notation for function types makes perfect sense in these cases.
我们可能不想完全记忆intor中的函数double。但是函数和数据类型之间的等价性(即使并不总是实用)是存在的。还有无限的类型,例如列表、字符串或树。对这些类型的函数的热切记忆将需要无限的存储空间。但 Haskell 是一种惰性语言,因此惰性求值(无限)数据结构和函数之间的界限是模糊的。这种函数与数据的二元性解释了 Haskell 函数类型与分类指数对象的识别——这更符合我们对数据的看法。
We probably wouldn’t want to fully memoize a function from int or double. But the equivalence between functions and data types, if not always practical, is there. There are also infinite types, for instance lists, strings, or trees. Eager memoization of functions from those types would require infinite storage. But Haskell is a lazy language, so the boundary between lazily evaluated (infinite) data structures and functions is fuzzy. This function vs. data duality explains the identification of Haskell’s function type with the categorical exponential object — which corresponds more to our idea of data.
尽管我将继续使用集合范畴作为类型和函数的模型,但值得一提的是,有一个更大的范畴系列可以用于此目的。这些范畴称为笛卡尔闭合,Set只是此类范畴的一个示例。
Although I will continue using the category of sets as a model for types and functions, it’s worth mentioning that there is a larger family of categories that can be used for that purpose. These categories are called cartesian closed, and Set is just one example of such a category.
笛卡尔封闭范畴必须包含:
A cartesian closed category must contain:
如果您将指数视为迭代乘积(可能无限次),那么您可以将笛卡尔闭范畴视为任意数量的支持乘积。特别是,终端对象可以被认为是零个对象的乘积——或者对象的零次方。
If you consider an exponential as an iterated product (possibly infinitely many times), then you can think of a cartesian closed category as one supporting products of an arbitrary arity. In particular, the terminal object can be thought of as a product of zero objects — or the zero-th power of an object.
从计算机科学的角度来看,笛卡尔封闭范畴的有趣之处在于它们为简单类型的 lambda 演算提供了模型,该演算构成了所有类型编程语言的基础。
What’s interesting about cartesian closed categories from the perspective of computer science is that they provide models for the simply typed lambda calculus, which forms the basis of all typed programming languages.
最终对象和乘积有其对偶:初始对象和余积。笛卡尔封闭范畴,也支持这两者,并且其中乘积可以分布在余积上
The terminal object and the product have their duals: the initial object and the coproduct. A cartesian closed category that also supports those two, and in which product can be distributed over coproduct
a × (b + c) = a × b + a × c
(b + c) × a = b × a + c × aa × (b + c) = a × b + a × c
(b + c) × a = b × a + c × a
称为双笛卡尔闭范畴。我们将在下一节中看到双笛卡尔封闭范畴(其中Set是一个主要示例)具有一些有趣的属性。
is called a bicartesian closed category. We’ll see in the next section that bicartesian closed categories, of which Set is a prime example, have some interesting properties.
将函数类型解释为指数非常适合代数数据类型的方案。事实证明,高中代数中与数字 0 和 1、和、乘积和指数相关的所有基本恒等式在任何双笛卡尔闭范畴论中几乎保持不变,分别针对初始和最终对象、余积、乘积和指数。我们还没有工具来证明它们(例如附加词或米田引理),但我还是将它们列在这里,作为有价值的直觉的来源。
The interpretation of function types as exponentials fits very well into the scheme of algebraic data types. It turns out that all the basic identities from high-school algebra relating numbers zero and one, sums, products, and exponentials hold pretty much unchanged in any bicartesian closed category theory for, respectively, initial and final objects, coproducts, products, and exponentials. We don’t have the tools yet to prove them (such as adjunctions or the Yoneda lemma), but I’ll list them here nevertheless as a source of valuable intuitions.
a0 = 1a0 = 1
在分类解释中,我们用初始对象替换 0,用最终对象替换 1,用同构替换相等。指数是内部 hom 对象。这个特定的指数表示从初始对象到任意对象的态射集合a。根据初始对象的定义,这样的态射恰好有一个,因此 hom 集C(0, a)是一个单例集。单例集是Set中的终端对象,因此这个标识在Set中很简单。我们所说的是它适用于任何双笛卡尔封闭范畴。
In the categorical interpretation, we replace 0 with the initial object, 1 with the final object, and equality with isomorphism. The exponential is the internal hom-object. This particular exponential represents the set of morphisms going from the initial object to an arbitrary object a. By the definition of the initial object, there is exactly one such morphism, so the hom-set C(0, a) is a singleton set. A singleton set is the terminal object in Set, so this identity trivially works in Set. What we are saying is that it works in any bicartesian closed category.
在 Haskell 中,我们将 0 替换为Void; 1 与单位类型();以及函数类型的指数。声明是Void任何类型的函数集都a等同于单元类型——它是一个单例。换句话说,只有一个功能Void->a。我们之前已经见过这个函数:它被称为absurd.
In Haskell, we replace 0 with Void; 1 with the unit type (); and the exponential with function type. The claim is that the set of functions from Void to any type a is equivalent to the unit type — which is a singleton. In other words, there is only one function Void->a. We’ve seen this function before: it’s called absurd.
这有点棘手,原因有两个。其一是,在 Haskell 中,我们并没有真正意义上的无人居住的类型——每种类型都包含“永无止境的计算的结果”,或者说底部。第二个原因是 的所有实现absurd都是等效的,因为无论它们做什么,没有人可以执行它们。没有可以传递给 的值absurd。(如果你设法向它传递一个永无止境的计算,它就永远不会返回!)
This is a little bit tricky, for two reasons. One is that in Haskell we don’t really have uninhabited types — every type contains the “result of a never ending calculation,” or the bottom. The second reason is that all implementations of absurd are equivalent because, no matter what they do, nobody can ever execute them. There is no value that can be passed to absurd. (And if you manage to pass it a never ending calculation, it will never return!)
1a = 11a = 1
当在Set中解释时,这个恒等式重述了终端对象的定义:从任何对象到终端对象都有唯一的态射。一般来说,从a终端对象到终端对象的内部 hom-object 与终端对象本身是同构的。
This identity, when interpreted in Set, restates the definition of the terminal object: There is a unique morphism from any object to the terminal object. In general, the internal hom-object from a to the terminal object is isomorphic to the terminal object itself.
在 Haskell 中,从任何类型到单元都只有一个函数a。我们以前见过这个函数——它叫做unit. 您也可以将其视为const部分应用于 的函数()。
In Haskell, there is only one function from any type a to unit. We’ve seen this function before — it’s called unit. You can also think of it as the function const partially applied to ().
a1 = aa1 = a
这是对来自终端对象的态射可用于挑选对象的“元素”的观察的重述a。此类态射的集合与对象本身同构。在Set和 Haskell 中,同构存在于集合的元素a和选取这些元素的函数之间()->a。
This is a restatement of the observation that morphisms from the terminal object can be used to pick “elements” of the object a. The set of such morphisms is isomorphic to the object itself. In Set, and in Haskell, the isomorphism is between elements of the set a and functions that pick those elements, ()->a.
ab+c = ab × acab+c = ab × ac
明确地说,这表示两个对象的余积的指数与两个指数的乘积同构。在 Haskell 中,这个代数恒等式有一个非常实用的解释。它告诉我们,两个类型之和的函数相当于单个类型的一对函数。这正是我们在定义求和函数时使用的案例分析。case我们通常不会用一条语句来编写一个函数定义,而是将其拆分为两个(或更多)函数,分别处理每个类型构造函数。例如,从 sum 类型中获取一个函数(Either Int Double):
Categorically, this says that the exponential from a coproduct of two objects is isomorphic to a product of two exponentials. In Haskell, this algebraic identity has a very practical, interpretation. It tells us that a function from a sum of two types is equivalent to a pair of functions from individual types. This is just the case analysis that we use when defining functions on sums. Instead of writing one function definition with a case statement, we usually split it into two (or more) functions dealing with each type constructor separately. For instance, take a function from the sum type (Either Int Double):
f :: Either Int Double -> Stringf :: Either Int Double -> String
它可以定义为一对函数,分别来自Int和Double:
It may be defined as a pair of functions from, respectively, Int and Double:
f (Left n) = if n < 0 then "Negative int" else "Positive int"
f (Right x) = if x < 0.0 then "Negative double" else "Positive double"f (Left n) = if n < 0 then "Negative int" else "Positive int"
f (Right x) = if x < 0.0 then "Negative double" else "Positive double"
这里,n是Int且x是Double。
Here, n is an Int and x is a Double.
(ab)c = ab×c(ab)c = ab×c
这只是纯粹用指数对象表达柯里化的一种方式。返回函数的函数相当于产品中的函数(双参数函数)。
This is just a way of expressing currying purely in terms of exponential objects. A function returning a function is equivalent to a function from a product (a two-argument function).
(a × b)c = ac × bc(a × b)c = ac × bc
在 Haskell 中:返回一对的函数相当于一对函数,每个函数生成该对中的一个元素。
In Haskell: A function returning a pair is equivalent to a pair of functions, each producing one element of the pair.
令人难以置信的是,这些简单的高中代数恒等式如何能够提升到范畴论并在函数式编程中得到实际应用。
It’s pretty incredible how those simple high-school algebraic identities can be lifted to category theory and have practical application in functional programming.
我已经提到了逻辑数据类型和代数数据类型之间的对应关系。类型Void和单元类型()对应的是 false 和 true。积类型和和类型对应于逻辑合取 ∧ (AND) 和析取 ⋁ (OR)。在这个方案中,我们刚刚定义的函数类型对应于逻辑蕴涵⇒。换句话说,该类型a->b可以理解为“if a then b”。
I have already mentioned the correspondence between logic and algebraic data types. The Void type and the unit type () correspond to false and true. Product types and sum types correspond to logical conjunction ∧ (AND) and disjunction ⋁ (OR). In this scheme, the function type we have just defined corresponds to logical implication ⇒. In other words, the type a->b can be read as “if a then b.”
根据库里-霍华德同构,每种类型都可以被解释为一个命题——一个可能为真或为假的陈述或判断。如果该类型有人居住,则这样的命题被认为是真;如果没有,则该命题被认为是假的。特别地,如果与其对应的函数类型被占据,则逻辑蕴涵为真,这意味着存在该类型的函数。因此,函数的实现就是定理的证明。编写程序相当于证明定理。让我们看几个例子。
According to the Curry-Howard isomorphism, every type can be interpreted as a proposition — a statement or a judgment that may be true or false. Such a proposition is considered true if the type is inhabited and false if it isn’t. In particular, a logical implication is true if the function type corresponding to it is inhabited, which means that there exists a function of that type. An implementation of a function is therefore a proof of a theorem. Writing programs is equivalent to proving theorems. Let’s see a few examples.
让我们以eval在函数对象的定义中引入的函数为例。它的签名是:
Let’s take the function eval we have introduced in the definition of the function object. Its signature is:
eval :: ((a -> b), a) -> beval :: ((a -> b), a) -> b
它采用由函数及其参数组成的对,并生成适当类型的结果。这是态射的 Haskell 实现:
It takes a pair consisting of a function and its argument and produces a result of the appropriate type. It’s the Haskell implementation of the morphism:
eval :: (a⇒b) × a -> beval :: (a⇒b) × a -> b
它定义了函数类型a⇒b(或指数对象ba)。让我们使用 Curry-Howard 同构将此签名转换为逻辑谓词:
which defines the function type a⇒b (or the exponential object ba). Let’s translate this signature to a logical predicate using the Curry-Howard isomorphism:
((a ⇒ b) ∧ a) ⇒ b((a ⇒ b) ∧ a) ⇒ b
您可以这样理解此语句: 如果、 和的b后继项为真,则必定为真。这具有完美的直觉意义,自古以来就被称为“肯定前件”( modus ponens)。我们可以通过实现函数来证明这个定理:aab
Here’s how you can read this statement: If it’s true that b follows from a, and a is true, then b must be true. This makes perfect intuitive sense and has been known since antiquity as modus ponens. We can prove this theorem by implementing the function:
eval :: ((a -> b), a) -> b
eval (f, x) = f xeval :: ((a -> b), a) -> b
eval (f, x) = f x
f如果您给我一对由接受a和返回的函数以及type 的b具体值组成的对,我可以通过简单地将函数应用于来生成 type 的具体值。通过实现这个函数,我刚刚证明了该类型是有人居住的。因此,在我们的逻辑中,肯定前件是正确的。xabfx((a -> b), a) -> b
If you give me a pair consisting of a function f taking a and returning b, and a concrete value x of type a, I can produce a concrete value of type b by simply applying the function f to x. By implementing this function I have just shown that the type ((a -> b), a) -> b is inhabited. Therefore modus ponens is true in our logic.
一个公然错误的谓词怎么样?例如:如果aorb为真,则a一定为真。
How about a predicate that is blatantly false? For instance: if a or b is true then a must be true.
a ⋁ b ⇒ aa ⋁ b ⇒ a
这显然是错误的,因为你可以选择一个a是假的,一个b是真的,这是一个反例。
This is obviously wrong because you can chose an a that is false and a b that is true, and that’s a counter-example.
使用 Curry-Howard 同构将此谓词映射到函数签名,我们得到:
Mapping this predicate into a function signature using the Curry-Howard isomorphism, we get:
Either a b -> aEither a b -> a
a尽管您可以尝试,但您无法实现此函数 -如果使用该值调用您,则无法生成该类型的值Right。(请记住,我们正在谈论纯函数。)
Try as you may, you can’t implement this function — you can’t produce a value of type a if you are called with the Right value. (Remember, we are talking about pure functions.)
最后我们来了解一下该函数的含义absurd:
Finally, we come to the meaning of the absurd function:
absurd :: Void -> aabsurd :: Void -> a
考虑到Void转化为 false,我们得到:
Considering that Void translates into false, we get:
false ⇒ afalse ⇒ a
任何事情都源于虚假(ex falso quodlibet)。这是 Haskell 中该语句(函数)的一个可能的证明(实现):
Anything follows from falsehood (ex falso quodlibet). Here’s one possible proof (implementation) of this statement (function) in Haskell:
absurd (Void a) = absurd aabsurd (Void a) = absurd a
其中Void定义为:
where Void is defined as:
newtype Void = Void Voidnewtype Void = Void Void
一如既往,这种类型Void很棘手。此定义使得无法构造一个值,因为为了构造一个值,您需要提供一个值。因此,该函数absurd永远无法被调用。
As always, the type Void is tricky. This definition makes it impossible to construct a value because in order to construct one, you would need to provide one. Therefore, the function absurd can never be called.
这些都是有趣的例子,但是库里-霍华德同构有实际的一面吗?日常编程中可能不会。但也有像 Agda 或 Coq 这样的编程语言,它们利用 Curry-Howard 同构来证明定理。
These are all interesting examples, but is there a practical side to Curry-Howard isomorphism? Probably not in everyday programming. But there are programming languages like Agda or Coq, which take advantage of the Curry-Howard isomorphism to prove theorems.
计算机不仅帮助数学家完成工作,而且正在彻底改变数学的基础。该领域最新的热门研究课题称为同伦类型理论,是类型理论的产物。它充满了布尔值、整数、乘积和余积、函数类型等等。而且,似乎是为了消除任何疑虑,该理论正在 Coq 和 Agda 中得到阐述。计算机正在以不止一种方式彻底改变世界。
Computers are not only helping mathematicians do their work — they are revolutionizing the very foundations of mathematics. The latest hot research topic in that area is called Homotopy Type Theory, and is an outgrowth of type theory. It’s full of Booleans, integers, products and coproducts, function types, and so on. And, as if to dispel any doubts, the theory is being formulated in Coq and Agda. Computers are revolutionizing the world in more than one way.
我要感谢 Gershom Bazerman 检查我的数学和逻辑,以及 André van Meulebrouck,他在这一系列帖子中自愿提供编辑帮助。
I’d like to thank Gershom Bazerman for checking my math and logic, and André van Meulebrouck, who has been volunteering his editing help throughout this series of posts.
我们将函子视为保留其结构的范畴之间的映射。函子将一个范畴“嵌入”另一个范畴。它可能会将多个事物合并为一个,但它永远不会破坏连接。一种思考方式是,通过函子,我们可以在一个范畴内建模另一个范畴。源范畴充当目标范畴一部分的某些结构的模型、蓝图。
We talked about functors as mappings between categories that preserve their structure. A functor “embeds” one category in another. It may collapse multiple things into one, but it never breaks connections. One way of thinking about it is that with a functor we are modeling one category inside another. The source category serves as a model, a blueprint, for some structure that’s part of the target category.
将一个范畴嵌入到另一个范畴中的方法可能有很多种。有时它们是相同的,有时却非常不同。一个可以将整个源范畴折叠成一个对象,另一个可以将每个对象映射到不同的对象,并将每个态射映射到不同的态射。同一个蓝图可以通过多种不同的方式实现。自然变换帮助我们比较这些认识。它们是函子的映射——保留其函子性质的特殊映射。
There may be many ways of embedding one category in another. Sometimes they are equivalent, sometimes very different. One may collapse the whole source category into one object, another may map every object to a different object and every morphism to a different morphism. The same blueprint may be realized in many different ways. Natural transformations help us compare these realizations. They are mappings of functors — special mappings that preserve their functorial nature.
考虑范畴C和D之间的两个函F子。如果您只关注C中的一个对象,它会映射到两个对象:和。因此,函子的映射应该映射到。GaF aG aF aG a
Consider two functors F and G between categories C and D. If you focus on just one object a in C, it is mapped to two objects: F a and G a. A mapping of functors should therefore map F a to G a.
请注意,F a和G a是同一范畴D中的对象。同一范畴中的对象之间的映射不应违背该范畴的粒度。我们不想在对象之间建立人为的联系。因此很自然地使用现有的联系,即态射。自然变换是态射的选择:对于每个对象a,它从F a到 中选择一个态射G a。如果我们将自然变换称为α,则该态射称为at或的分量。αaαa
Notice that F a and G a are objects in the same category D. Mappings between objects in the same category should not go against the grain of the category. We don’t want to make artificial connections between objects. So it’s natural to use existing connections, namely morphisms. A natural transformation is a selection of morphisms: for every object a, it picks one morphism from F a to G a. If we call the natural transformation α, this morphism is called the component of α at a, or αa.
αa :: F a -> G aαa :: F a -> G a
请记住,是Ca中的对象,而是D中的态射。αa
Keep in mind that a is an object in C while αa is a morphism in D.
如果对于某些, D中和 之间a不存在态射,则和 之间不可能存在自然变换。F aG aFG
If, for some a, there is no morphism between F a and G a in D, there can be no natural transformation between F and G.
当然,这只是故事的一半,因为函子不仅映射对象,还映射态射。那么自然变换对这些映射有何作用呢?事实证明,态射的映射是固定的——在 F 和 G 之间的任何自然变换下,都F f必须变换为G f。更重要的是,两个函子对态射的映射极大地限制了我们在定义与其兼容的自然变换时的选择。考虑C中f两个对象a和之间的态射。它映射到两个态射,在D中:bF fG f
Of course that’s only half of the story, because functors not only map objects, they map morphisms as well. So what does a natural transformation do with those mappings? It turns out that the mapping of morphisms is fixed — under any natural transformation between F and G, F f must be transformed into G f. What’s more, the mapping of morphisms by the two functors drastically restricts the choices we have in defining a natural transformation that’s compatible with it. Consider a morphism f between two objects a and b in C. It’s mapped to two morphisms, F f and G f in D:
F f :: F a -> F b
G f :: G a -> G bF f :: F a -> F b
G f :: G a -> G b
自然变换提供了两个额外的态射来完成Dα中的图:
The natural transformation α provides two additional morphisms that complete the diagram in D:
αa :: F a -> G a
αb :: F b -> G bαa :: F a -> G a
αb :: F b -> G b
现在我们有两种方法从F a到G b。为了确保它们相等,我们必须施加适用于任何 的自然性条件f:
Now we have two ways of getting from F a to G b. To make sure that they are equal, we must impose the naturality condition that holds for any f:
G f ∘ αa = αb ∘ F fG f ∘ αa = αb ∘ F f
自然性条件是一个相当严格的要求。例如,如果态射F f是可逆的,则自然性由αb决定αa。它运输 :αa_f
The naturality condition is a pretty stringent requirement. For instance, if the morphism F f is invertible, naturality determines αb in terms of αa. It transports αa along f:
αb = (G f) ∘ αa ∘ (F f)-1αb = (G f) ∘ αa ∘ (F f)-1
如果两个对象之间存在多个可逆态射,则所有这些传输必须一致。但一般来说,态射是不可逆的。但你可以看到,两个函子之间自然变换的存在远不能得到保证。因此,通过自然变换相关的函子的稀缺或丰富可能会告诉你很多关于它们之间运行的范畴的结构。当我们讨论极限和米田引理时,我们会看到一些例子。
If there is more than one invertible morphism between two objects, all these transports have to agree. In general, though, morphisms are not invertible; but you can see that the existence of natural transformations between two functors is far from guaranteed. So the scarcity or the abundance of functors that are related by natural transformations may tell you a lot about the structure of categories between which they operate. We’ll see some examples of that when we talk about limits and the Yoneda lemma.
从组件角度来看自然变换,人们可能会说它将对象映射到态射。由于自然性条件,我们也可以说它将态射映射到交换平方 —对于C中的每个态射, D中都有一个交换自然性平方。
Looking at a natural transformation component-wise, one may say that it maps objects to morphisms. Because of the naturality condition, one may also say that it maps morphisms to commuting squares — there is one commuting naturality square in D for every morphism in C.
自然变换的这种特性在许多分类结构中非常有用,其中通常包括通勤图。通过明智地选择函子,许多交换性条件可以转化为自然性条件。当我们讨论极限、余极限和附加时,我们会看到这样的例子。
This property of natural transformations comes in very handy in a lot of categorical constructions, which often include commuting diagrams. With a judicious choice of functors, a lot of these commutativity conditions may be transformed into naturality conditions. We’ll see examples of that when we get to limits, colimits, and adjunctions.
最后,自然变换可用于定义函子的同构。说两个函子自然同构几乎就像说它们是相同的。自然同构被定义为一个自然变换,其组成部分都是同构(可逆态射)。
Finally, natural transformations may be used to define isomorphisms of functors. Saying that two functors are naturally isomorphic is almost like saying they are the same. Natural isomorphism is defined as a natural transformation whose components are all isomorphisms (invertible morphisms).
我们讨论了函子(或更具体地说,endofunctors)在编程中的作用。它们对应于将类型映射到类型的类型构造函数。它们还将函数映射到函数,并且这种映射是由高阶函数fmap(或C++ 中的transform、then等)实现的。
We talked about the role of functors (or, more specifically, endofunctors) in programming. They correspond to type constructors that map types to types. They also map functions to functions, and this mapping is implemented by a higher order function fmap (or transform, then, and the like in C++).
为了构造一个自然转换,我们从一个对象开始,这里是一个类型,a。一个函子 ,F将其映射到类型F a。另一个函子 ,G将其映射到G a。alpha自然变换at的分量是从到 的a函数。在伪 Haskell 中:F aG a
To construct a natural transformation we start with an object, here a type, a. One functor, F, maps it to the type F a. Another functor, G, maps it to G a. The component of a natural transformation alpha at a is a function from F a to G a. In pseudo-Haskell:
alphaa :: F a -> G aalphaa :: F a -> G a
自然变换是为所有类型定义的多态函数a:
A natural transformation is a polymorphic function that is defined for all types a:
alpha :: forall a . F a -> G aalpha :: forall a . F a -> G a
在 Haskell 中是forall a可选的(实际上需要打开语言扩展ExplicitForAll)。通常,你会这样写:
The forall a is optional in Haskell (and in fact requires turning on the language extension ExplicitForAll). Normally, you would write it like this:
alpha :: F a -> G aalpha :: F a -> G a
请记住,它实际上是由 参数化的一系列函数a。这是 Haskell 语法简洁性的另一个例子。C++ 中的类似构造会稍微冗长一些:
Keep in mind that it’s really a family of functions parameterized by a. This is another example of the terseness of the Haskell syntax. A similar construct in C++ would be slightly more verbose:
template<class A> G<A> alpha(F<A>);template<class A> G<A> alpha(F<A>);
Haskell 的多态函数和 C++ 泛型函数之间存在更深刻的区别,这反映在这些函数的实现和类型检查的方式上。在 Haskell 中,必须为所有类型统一定义多态函数。一种公式必须适用于所有类型。这称为参数多态性。
There is a more profound difference between Haskell’s polymorphic functions and C++ generic functions, and it’s reflected in the way these functions are implemented and type-checked. In Haskell, a polymorphic function must be defined uniformly for all types. One formula must work across all types. This is called parametric polymorphism.
另一方面,C++ 默认支持临时多态性,这意味着模板不必针对所有类型进行良好定义。模板是否适用于给定类型是在实例化时决定的,其中用具体类型替换类型参数。类型检查被推迟,不幸的是,这通常会导致难以理解的错误消息。
C++, on the other hand, supports by default ad hoc polymorphism, which means that a template doesn’t have to be well-defined for all types. Whether a template will work for a given type is decided at instantiation time, where a concrete type is substituted for the type parameter. Type checking is deferred, which unfortunately often leads to incomprehensible error messages.
在C++中,还有函数重载和模板特化的机制,允许对不同类型对同一函数进行不同的定义。在 Haskell 中,此功能由类型类和类型族提供。
In C++, there is also a mechanism for function overloading and template specialization, which allows different definitions of the same function for different types. In Haskell this functionality is provided by type classes and type families.
Haskell 的参数多态性有一个意想不到的结果:任何以下类型的多态函数:
Haskell’s parametric polymorphism has an unexpected consequence: any polymorphic function of the type:
alpha :: F a -> G aalpha :: F a -> G a
其中F和G是函子,自动满足自然性条件。这是分类符号(f是一个函数f::a->b):
where F and G are functors, automatically satisfies the naturality condition. Here it is in categorical notation (f is a function f::a->b):
G f ∘ αa = αb ∘ F fG f ∘ αa = αb ∘ F f
G在 Haskell 中,函子对态射的作用f是使用 实现的fmap。我将首先用伪 Haskell 编写它,并带有显式类型注释:
In Haskell, the action of a functor G on a morphism f is implemented using fmap. I’ll first write it in pseudo-Haskell, with explicit type annotations:
fmapG f . alphaa = alphab . fmapF ffmapG f . alphaa = alphab . fmapF f
由于类型推断,这些注释不是必需的,并且以下等式成立:
Because of type inference, these annotations are not necessary, and the following equation holds:
fmap f . alpha = alpha . fmap ffmap f . alpha = alpha . fmap f
这仍然不是真正的 Haskell——函数相等不能用代码来表达——但它是程序员可以在等式推理中使用的恒等式;或者由编译器来实现优化。
This is still not real Haskell — function equality is not expressible in code — but it’s an identity that can be used by the programmer in equational reasoning; or by the compiler, to implement optimizations.
自然性条件在 Haskell 中是自动的,这与“免费定理”有关。参数多态性用于定义 Haskell 中的自然变换,它对实现施加了非常强的限制——一个公式适用于所有类型。这些限制转化为关于此类函数的方程定理。对于变换函子的函数,自由定理是自然性条件。[您可以在我的博客《参数化:无用金钱》和《免费定理》中阅读有关免费定理的更多信息。]
The reason why the naturality condition is automatic in Haskell has to do with “theorems for free.” Parametric polymorphism, which is used to define natural transformations in Haskell, imposes very strong limitations on the implementation — one formula for all types. These limitations translate into equational theorems about such functions. In the case of functions that transform functors, free theorems are the naturality conditions. [You may read more about free theorems in my blog Parametricity: Money for Nothing and Theorems for Free.]
我之前提到的 Haskell 中函子的一种思考方式是将它们视为广义容器。我们可以继续这个类比,并将自然转换视为将一个容器的内容重新包装到另一个容器中的方法。我们不会触及项目本身:我们不会修改它们,也不会创建新的项目。我们只是将它们(其中一些)复制到一个新容器中,有时复制多次。
One way of thinking about functors in Haskell that I mentioned earlier is to consider them generalized containers. We can continue this analogy and consider natural transformations to be recipes for repackaging the contents of one container into another container. We are not touching the items themselves: we don’t modify them, and we don’t create new ones. We are just copying (some of) them, sometimes multiple times, into a new container.
自然性条件变成了这样的陈述:无论我们是否先修改项目,通过应用fmap,然后再重新打包;或者先重新打包,然后修改新容器中的项目,用它自己的fmap. fmap重新打包和ping这两个操作是正交的。“一个移动鸡蛋,另一个煮鸡蛋。”
The naturality condition becomes the statement that it doesn’t matter whether we modify the items first, through the application of fmap, and repackage later; or repackage first, and then modify the items in the new container, with its own implementation of fmap. These two actions, repackaging and fmapping, are orthogonal. “One moves the eggs, the other boils them.”
让我们看几个 Haskell 中自然变换的例子。第一个是列表函子和Maybe函子之间。它返回列表的头部,但前提是列表非空:
Let’s see a few examples of natural transformations in Haskell. The first is between the list functor, and the Maybe functor. It returns the head of the list, but only if the list is non-empty:
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:xs) = Just xsafeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:xs) = Just x
它是 中的多态函数a。它适用于任何类型a,没有任何限制,因此它是参数多态性的一个例子。因此,这是两个函子之间的自然变换。但为了说服自己,让我们验证一下自然性条件。
It’s a function polymorphic in a. It works for any type a, with no limitations, so it is an example of parametric polymorphism. Therefore it is a natural transformation between the two functors. But just to convince ourselves, let’s verify the naturality condition.
fmap f . safeHead = safeHead . fmap ffmap f . safeHead = safeHead . fmap f
我们有两种情况需要考虑;一个空列表:
We have two cases to consider; an empty list:
fmap f (safeHead []) = fmap f Nothing = Nothingfmap f (safeHead []) = fmap f Nothing = Nothing
safeHead (fmap f []) = safeHead [] = NothingsafeHead (fmap f []) = safeHead [] = Nothing
和一个非空列表:
and a non-empty list:
fmap f (safeHead (x:xs)) = fmap f (Just x) = Just (f x)fmap f (safeHead (x:xs)) = fmap f (Just x) = Just (f x)
safeHead (fmap f (x:xs)) = safeHead (f x : fmap f xs) = Just (f x)safeHead (fmap f (x:xs)) = safeHead (f x : fmap f xs) = Just (f x)
我使用了 for 列表的实现fmap:
I used the implementation of fmap for lists:
fmap f [] = []
fmap f (x:xs) = f x : fmap f xsfmap f [] = []
fmap f (x:xs) = f x : fmap f xs
和对于Maybe:
and for Maybe:
fmap f Nothing = Nothing
fmap f (Just x) = Just (f x)fmap f Nothing = Nothing
fmap f (Just x) = Just (f x)
一个有趣的情况是其中一个函子是平凡Const函子。从函子到Const函子的自然转换看起来就像一个函数,其返回类型或其参数类型是多态的。
An interesting case is when one of the functors is the trivial Const functor. A natural transformation from or to a Const functor looks just like a function that’s either polymorphic in its return type or in its argument type.
例如,length可以被认为是从列表函子到Const Int函子的自然转换:
For instance, length can be thought of as a natural transformation from the list functor to the Const Int functor:
length :: [a] -> Const Int a
length [] = Const 0
length (x:xs) = Const (1 + unConst (length xs))length :: [a] -> Const Int a
length [] = Const 0
length (x:xs) = Const (1 + unConst (length xs))
这里,unConst用于剥离Const构造函数:
Here, unConst is used to peel off the Const constructor:
unConst :: Const c a -> c
unConst (Const x) = xunConst :: Const c a -> c
unConst (Const x) = x
当然,实际中length定义为:
Of course, in practice length is defined as:
length :: [a] -> Intlength :: [a] -> Int
这有效地掩盖了这是自然转变的事实。
which effectively hides the fact that it’s a natural transformation.
从函子中找到参数多态函数Const有点困难,因为它需要从无到有创建一个值。我们能做的最好的就是:
Finding a parametrically polymorphic function from a Const functor is a little harder, since it would require the creation of a value from nothing. The best we can do is:
scam :: Const Int a -> Maybe a
scam (Const x) = Nothingscam :: Const Int a -> Maybe a
scam (Const x) = Nothing
我们已经见过的另一个常见函子是函子,它将在米田引理中发挥重要作用Reader。我将其定义重写为newtype:
Another common functor that we’ve seen already, and which will play an important role in the Yoneda lemma, is the Reader functor. I will rewrite its definition as a newtype:
newtype Reader e a = Reader (e -> a)newtype Reader e a = Reader (e -> a)
它由两种类型参数化,但仅在第二种类型中(协变)函数:
It is parameterized by two types, but is (covariantly) functorial only in the second one:
instance Functor (Reader e) where
fmap f (Reader g) = Reader (\x -> f (g x))instance Functor (Reader e) where
fmap f (Reader g) = Reader (\x -> f (g x))
对于每种类型e,您都可以定义一系列从Reader e到任何其他函子的自然转换f。f e稍后我们会看到,这个家族的成员总是与(米田引理)的元素一一对应。
For every type e, you can define a family of natural transformations from Reader e to any other functor f. We’ll see later that the members of this family are always in one to one correspondence with the elements of f e (the Yoneda lemma).
例如,考虑()具有一个元素的有点琐碎的单元类型()。函子Reader ()接受任何类型a并将其映射为函数类型()->a。这些只是从集合中选取单个元素的所有函数a。其中有多少个元素就有多少个a。现在让我们考虑从这个函子到Maybe函子的自然变换:
For instance, consider the somewhat trivial unit type () with one element (). The functor Reader () takes any type a and maps it into a function type ()->a. These are just all the functions that pick a single element from the set a. There are as many of these as there are elements in a. Now let’s consider natural transformations from this functor to the Maybe functor:
alpha :: Reader () a -> Maybe aalpha :: Reader () a -> Maybe a
只有其中两个,dumb并且obvious:
There are only two of these, dumb and obvious:
dumb (Reader _) = Nothingdumb (Reader _) = Nothing
和
and
obvious (Reader g) = Just (g ())obvious (Reader g) = Just (g ())
(您唯一能做的g就是将其应用于单位值()。)
(The only thing you can do with g is to apply it to the unit value ().)
事实上,正如米田引理所预测的那样,它们对应于该Maybe ()类型的两个元素,即Nothing和Just ()。稍后我们会回到米田引理——这只是一个小预告。
And, indeed, as predicted by the Yoneda lemma, these correspond to the two elements of the Maybe () type, which are Nothing and Just (). We’ll come back to the Yoneda lemma later — this was just a little teaser.
两个函子之间的参数多态函数(包括函子的边缘情况Const)始终是自然变换。由于所有标准代数数据类型都是函子,因此此类类型之间的任何多态函数都是自然转换。
A parametrically polymorphic function between two functors (including the edge case of the Const functor) is always a natural transformation. Since all standard algebraic data types are functors, any polymorphic function between such types is a natural transformation.
我们还可以使用函数类型,它们的返回类型是函数式的。我们可以使用它们来构建函子(如Reader函子)并定义作为高阶函数的自然变换。
We also have function types at our disposal, and those are functorial in their return type. We can use them to build functors (like the Reader functor) and define natural transformations that are higher-order functions.
但是,函数类型在参数类型中不协变。它们是逆变的。当然,逆变函子等价于相反范畴的协变函子。两个逆变函子之间的多态函数仍然是范畴意义上的自然变换,只不过它们作用于与 Haskell 类型相反范畴的函子。
However, function types are not covariant in the argument type. They are contravariant. Of course contravariant functors are equivalent to covariant functors from the opposite category. Polymorphic functions between two contravariant functors are still natural transformations in the categorical sense, except that they work on functors from the opposite category to Haskell types.
您可能还记得我们之前看过的逆变函子的示例:
You might remember the example of a contravariant functor we’ve looked at before:
newtype Op r a = Op (a -> r)newtype Op r a = Op (a -> r)
该函子在 中是逆变的a:
This functor is contravariant in a:
instance Contravariant (Op r) where
contramap f (Op g) = Op (g . f)instance Contravariant (Op r) where
contramap f (Op g) = Op (g . f)
我们可以编写一个多态函数,例如Op Bool从 to Op String:
We can write a polymorphic function from, say, Op Bool to Op String:
predToStr (Op f) = Op (\x -> if f x then "T" else "F")predToStr (Op f) = Op (\x -> if f x then "T" else "F")
但由于两个函子不是协变的,这不是Hask中的自然变换。然而,因为它们都是逆变的,所以它们满足“相反”的自然性条件:
But since the two functors are not covariant, this is not a natural transformation in Hask. However, because they are both contravariant, they satisfy the “opposite” naturality condition:
contramap f . predToStr = predToStr . contramap fcontramap f . predToStr = predToStr . contramap f
请注意,由于 的签名,该函数的方向f必须与您使用的方向相反:fmapcontramap
Notice that the function f must go in the opposite direction than what you’d use with fmap, because of the signature of contramap:
contramap :: (b -> a) -> (Op Bool a -> Op Bool b)contramap :: (b -> a) -> (Op Bool a -> Op Bool b)
是否存在不是函子的类型构造函数,无论是协变还是逆变?这是一个例子:
Are there any type constructors that are not functors, whether covariant or contravariant? Here’s one example:
a -> aa -> a
这不是函子,因为a负(逆变)和正(协变)位置都使用相同的类型。您无法为这种类型实现fmapor 。contramap因此签名的函数:
This is not a functor because the same type a is used both in the negative (contravariant) and positive (covariant) position. You can’t implement fmap or contramap for this type. Therefore a function of the signature:
(a -> a) -> f a(a -> a) -> f a
其中f是任意函子,不能是自然变换。有趣的是,有一个自然变换的概括,称为自然变换,可以处理这种情况。当我们讨论结束时我们会找到他们。
where f is an arbitrary functor, cannot be a natural transformation. Interestingly, there is a generalization of natural transformations, called dinatural transformations, that deals with such cases. We’ll get to them when we discuss ends.
既然我们有了函子之间的映射(自然变换),那么很自然地就会问函子是否形成一个范畴的问题。确实如此!每对范畴 C 和 D 都有一个函子范畴。该范畴中的对象是从 C 到 D 的函子,态射是这些函子之间的自然变换。
Now that we have mappings between functors — natural transformations — it’s only natural to ask the question whether functors form a category. And indeed they do! There is one category of functors for each pair of categories, C and D. Objects in this category are functors from C to D, and morphisms are natural transformations between those functors.
我们必须定义两个自然变换的组合,但这很容易。自然变换的组成部分是态射,我们知道如何合成态射。
We have to define composition of two natural transformations, but that’s quite easy. The components of natural transformations are morphisms, and we know how to compose morphisms.
事实上,让我们采用从函子 F 到 G 的自然变换 α 。它在对象处的分量a是某种态射:
Indeed, let’s take a natural transformation α from functor F to G. Its component at object a is some morphism:
αa :: F a -> G aαa :: F a -> G a
我们想将 α 与 β 组合起来,这是从函子 G 到 H 的自然变换。 β at 的分量a是一个态射:
We’d like to compose α with β, which is a natural transformation from functor G to H. The component of β at a is a morphism:
βa :: G a -> H aβa :: G a -> H a
这些态射是可组合的,它们的组合是另一种态射:
These morphisms are composable and their composition is another morphism:
βa ∘ αa :: F a -> H aβa ∘ αa :: F a -> H a
我们将使用这个态射作为自然变换 β ⋅ α 的分量——α 之后的两个自然变换 β 的组合:
We will use this morphism as the component of the natural transformation β ⋅ α — the composition of two natural transformations β after α:
(β ⋅ α)a = βa ∘ αa(β ⋅ α)a = βa ∘ αa
仔细观察一下图表,我们就会相信这个组合的结果确实是从 F 到 H 的自然变换:
One (long) look at a diagram convinces us that the result of this composition is indeed a natural transformation from F to H:
H f ∘ (β ⋅ α)a = (β ⋅ α)b ∘ F fH f ∘ (β ⋅ α)a = (β ⋅ α)b ∘ F f
自然变换的组合是结合律的,因为它们的分量(即正则态射)就其组合而言是结合律的。
Composition of natural transformations is associative, because their components, which are regular morphisms, are associative with respect to their composition.
最后,对于每个函子 F 都有一个恒等自然变换 1 F,其分量是恒等态射:
Finally, for each functor F there is an identity natural transformation 1F whose components are the identity morphisms:
idF a :: F a -> F aidF a :: F a -> F a
因此,函子确实形成了一个范畴。
So, indeed, functors form a category.
关于符号的一句话。遵循 Saunders Mac Lane,我使用点来表示我刚才描述的那种自然变换构图。问题是有两种构成自然变换的方法。这称为垂直组合,因为函子通常在描述它的图中垂直堆叠。垂直组合对于定义函子范畴很重要。我将很快解释水平构图。
A word about notation. Following Saunders Mac Lane I use the dot for the kind of natural transformation composition I have just described. The problem is that there are two ways of composing natural transformations. This one is called the vertical composition, because the functors are usually stacked up vertically in the diagrams that describe it. Vertical composition is important in defining the functor category. I’ll explain horizontal composition shortly.
范畴 C 和 D 之间的函子范畴可写为Fun(C, D), or [C, D],或有时写为DC。最后一个表示法表明函子范畴本身可能被视为某个其他范畴中的函数对象(指数)。情况确实如此吗?
The functor category between categories C and D is written as Fun(C, D), or [C, D], or sometimes as DC. This last notation suggests that a functor category itself might be considered a function object (an exponential) in some other category. Is this indeed the case?
让我们看一下到目前为止我们已经构建的抽象层次结构。我们从一个范畴开始,它是对象和态射的集合。范畴本身(或者严格来说,小范畴,其对象形成集合)本身就是更高级别范畴Cat中的对象。该范畴中的态射是函子。Cat中的 Hom-set是函子的集合。例如 Cat(C, D) 是两个范畴 C 和 D 之间的一组函子。
Let’s have a look at the hierarchy of abstractions that we’ve been building so far. We started with a category, which is a collection of objects and morphisms. Categories themselves (or, strictly speaking small categories, whose objects form sets) are themselves objects in a higher-level category Cat. Morphisms in that category are functors. A Hom-set in Cat is a set of functors. For instance Cat(C, D) is a set of functors between two categories C and D.
函子范畴 [C, D] 也是两个范畴之间的一组函子(加上作为态射的自然变换)。它的对象与Cat(C,D)的成员相同。而且,一个函子范畴作为一个范畴,它本身一定是Cat的一个对象(恰好两个小范畴之间的函子范畴本身就很小)。我们在范畴中的 Hom 集和同一范畴中的对象之间存在关系。这种情况与我们在上一节中看到的指数对象完全相同。让我们看看如何在Cat中构造后者。
A functor category [C, D] is also a set of functors between two categories (plus natural transformations as morphisms). Its objects are the same as the members of Cat(C, D). Moreover, a functor category, being a category, must itself be an object of Cat (it so happens that the functor category between two small categories is itself small). We have a relationship between a Hom-set in a category and an object in the same category. The situation is exactly like the exponential object that we’ve seen in the last section. Let’s see how we can construct the latter in Cat.
您可能还记得,为了构建指数,我们需要首先定义一个乘积。在Cat中,这相对容易,因为小范畴是对象的集合,并且我们知道如何定义集合的笛卡尔积。因此,产品范畴 C × D 中的对象只是一对对象 ,(c, d)一个来自 C,一个来自 D。类似地,两个这样的对之间的态射(c, d)和(c', d')是一对态射 ,(f, g)其中f :: c -> c'和g :: d -> d'。这些态射对按分量组合,并且始终存在一个恒等对,它只是一对恒等态射。长话短说,Cat是一个成熟的笛卡尔封闭范畴,其中任何一对范畴都存在一个指数对象 D C 。Cat中的“对象”指的是一个范畴,所以 D C是一个范畴,我们可以将其等同于 C 和 D 之间的函子范畴。
As you may remember, in order to construct an exponential, we need to first define a product. In Cat, this turns out to be relatively easy, because small categories are sets of objects, and we know how to define cartesian products of sets. So an object in a product category C × D is just a pair of objects, (c, d), one from C and one from D. Similarly, a morphism between two such pairs, (c, d) and (c', d'), is a pair of morphisms, (f, g), where f :: c -> c' and g :: d -> d'. These pairs of morphisms compose component-wise, and there is always an identity pair that is just a pair of identity morphisms. To make the long story short, Cat is a full-blown cartesian closed category in which there is an exponential object DC for any pair of categories. And by “object” in Cat I mean a category, so DC is a category, which we can identify with the functor category between C and D.
抛开这些,让我们仔细看看Cat。根据定义, Cat中的任何 Hom 集都是函子集。但是,正如我们所看到的,两个对象之间的函子具有比集合更丰富的结构。它们形成一个范畴,自然变换充当态射。由于函子在Cat中被视为态射,因此自然变换是态射之间的态射。
With that out of the way, let’s have a closer look at Cat. By definition, any Hom-set in Cat is a set of functors. But, as we have seen, functors between two objects have a richer structure than just a set. They form a category, with natural transformations acting as morphisms. Since functors are considered morphisms in Cat, natural transformations are morphisms between morphisms.
这个更丰富的结构是 2-范畴的一个例子,它是一个范畴的概括,其中除了对象和态射(在本文中可能称为 1-态射)之外,还有 2-态射,即态射之间的态射。
This richer structure is an example of a 2-category, a generalization of a category where, besides objects and morphisms (which might be called 1-morphisms in this context), there are also 2-morphisms, which are morphisms between morphisms.
对于Cat被视为 2 类的情况,我们有:
In the case of Cat seen as a 2-category we have:
我们有一个 Hom 范畴,而不是两个范畴 C 和 D 之间的 Hom 集合,即函子范畴 D C。我们有常规的函子组合:来自 D C的函子 F与来自 E D的函子 G 组合,得到来自 E C的 G ∘ F 。但我们在每个 Hom 范畴内也有组合——函子之间自然变换或 2-态射的垂直组合。
Instead of a Hom-set between two categories C and D, we have a Hom-category — the functor category DC. We have regular functor composition: a functor F from DC composes with a functor G from ED to give G ∘ F from EC. But we also have composition inside each Hom-category — vertical composition of natural transformations, or 2-morphisms, between functors.
对于 2 范畴中的两种组合,出现了问题:它们如何相互作用?
With two kinds of composition in a 2-category, the question arises: How do they interact with each other?
让我们在Cat中选择两个函子或 1-态射:
Let’s pick two functors, or 1-morphisms, in Cat:
F :: C -> D
G :: D -> EF :: C -> D
G :: D -> E
及其组成:
and their composition:
G ∘ F :: C -> EG ∘ F :: C -> E
假设我们有两个自然变换 α 和 β,分别作用于函子 F 和 G:
Suppose we have two natural transformations, α and β, that act, respectively, on functors F and G:
α :: F -> F'
β :: G -> G'α :: F -> F'
β :: G -> G'
请注意,我们不能对这一对应用垂直合成,因为 α 的目标与 β 的源不同。事实上,它们是两个不同函子范畴的成员:D C和 E D。然而,我们可以将复合应用于函子 F' 和 G',因为 F' 的目标是 G' 的源 - 它是范畴 D。函子 G'∘ F' 和 G ∘ F 之间有什么关系?
Notice that we cannot apply vertical composition to this pair, because the target of α is different from the source of β. In fact they are members of two different functor categories: D C and E D. We can, however, apply composition to the functors F’ and G’, because the target of F’ is the source of G’ — it’s the category D. What’s the relation between the functors G’∘ F’ and G ∘ F?
有了 α 和 β,我们可以定义从 G ∘ F 到 G'∘ F' 的自然变换吗?让我画一下结构图。
Having α and β at our disposal, can we define a natural transformation from G ∘ F to G’∘ F’? Let me sketch the construction.
像往常一样,我们从 C 中的一个对象开始。a它的图像分为 D:F a和中的两个对象F'a。还有一个态射,α 的一个分量,连接这两个对象:
As usual, we start with an object a in C. Its image splits into two objects in D: F a and F'a. There is also a morphism, a component of α, connecting these two objects:
αa :: F a -> F'aαa :: F a -> F'a
当从 D 到 E 时,这两个对象进一步分裂为四个对象:
When going from D to E, these two objects split further into four objects:
G (F a), G'(F a), G (F'a), G'(F'a)G (F a), G'(F a), G (F'a), G'(F'a)
我们还有四个态射形成一个正方形。其中两个态射是自然变换 β 的组成部分:
We also have four morphisms forming a square. Two of these morphisms are the components of the natural transformation β:
βF a :: G (F a) -> G'(F a)
βF'a :: G (F'a) -> G'(F'a)βF a :: G (F a) -> G'(F a)
βF'a :: G (F'a) -> G'(F'a)
另外两个是 α a在两个函子下的图像(函子映射态射):
The other two are the images of αa under the two functors (functors map morphisms):
G αa :: G (F a) -> G (F'a)
G'αa :: G'(F a) -> G'(F'a)G αa :: G (F a) -> G (F'a)
G'αa :: G'(F a) -> G'(F'a)
这是很多态射。我们的目标是找到从 到 的态射G (F a),G'(F'a)它是连接两个函子 G ∘ F 和 G'∘ F' 的自然变换分量的候选者。事实上,从G (F a)到 ,我们可以采取的不是一条而是两条路径G'(F'a):
That’s a lot of morphisms. Our goal is to find a morphism that goes from G (F a) to G'(F'a), a candidate for the component of a natural transformation connecting the two functors G ∘ F and G’∘ F’. In fact there’s not one but two paths we can take from G (F a) to G'(F'a):
G'αa ∘ βF a
βF'a ∘ G αaG'αa ∘ βF a
βF'a ∘ G αa
对我们来说幸运的是,它们是相等的,因为我们形成的平方结果是 β 的自然平方。
Luckily for us, they are equal, because the square we have formed turns out to be the naturality square for β.
我们刚刚定义了从 G ∘ F 到 G'∘ F' 的自然变换的一个组成部分。只要您有足够的耐心,这种转变的自然性证明就非常简单。
We have just defined a component of a natural transformation from G ∘ F to G’∘ F’. The proof of naturality for this transformation is pretty straightforward, provided you have enough patience.
我们将这种自然变换称为α 和 β 的水平组合:
We call this natural transformation the horizontal composition of α and β:
β ∘ α :: G ∘ F -> G'∘ F'β ∘ α :: G ∘ F -> G'∘ F'
再次,遵循Mac Lane,我使用小圆圈进行水平构图,尽管你也可能会在它的位置遇到星星。
Again, following Mac Lane I use the small circle for horizontal composition, although you may also encounter star in its place.
这是一个分类的经验法则:每次你有构图时,你都应该寻找一个范畴。我们有自然变换的垂直组合,它是函子范畴的一部分。但是横向构图呢?它属于什么范畴?
Here’s a categorical rule of thumb: Every time you have composition, you should look for a category. We have vertical composition of natural transformations, and it’s part of the functor category. But what about the horizontal composition? What category does that live in?
解决这个问题的方法是从侧面看Cat。不要将自然变换视为函子之间的箭头,而是范畴之间的箭头。自然变换位于两个范畴之间,这两个范畴通过它变换的函子连接起来。我们可以将其视为连接这两个范畴。
The way to figure this out is to look at Cat sideways. Look at natural transformations not as arrows between functors but as arrows between categories. A natural transformation sits between two categories, the ones that are connected by the functors it transforms. We can think of it as connecting these two categories.
让我们关注Cat的两个对象——范畴 C 和 D。连接 C 到 D 的函子之间有一组自然变换。这些自然变换是我们从 C 到 D 的新箭头。同样,有自然变换连接 D 到 E 的函子之间的变换,我们可以将其视为从 D 到 E 的新箭头。水平组合是这些箭头的组合。
Let’s focus on two objects of Cat — categories C and D. There is a set of natural transformations that go between functors that connect C to D. These natural transformations are our new arrows from C to D. By the same token, there are natural transformations going between functors that connect D to E, which we can treat as new arrows going from D to E. Horizontal composition is the composition of these arrows.
我们还有一个从 C 到 C 的恒等箭头。恒等自然变换将 C 上的恒等函子映射到其自身。请注意,水平构图的恒等式也是垂直构图的恒等式,但反之则不然。
We also have an identity arrow going from C to C. It’s the identity natural transformation that maps the identity functor on C to itself. Notice that the identity for horizontal composition is also the identity for vertical composition, but not vice versa.
最后,这两个组合满足交换律:
Finally, the two compositions satisfy the interchange law:
(β' ⋅ α') ∘ (β ⋅ α) = (β' ∘ β) ⋅ (α' ∘ α)(β' ⋅ α') ∘ (β ⋅ α) = (β' ∘ β) ⋅ (α' ∘ α)
我将在这里引用桑德斯·麦克莱恩的话:读者可能喜欢写下证明这一事实所需的明显图表。
I will quote Saunders Mac Lane here: The reader may enjoy writing down the evident diagrams needed to prove this fact.
还有一种符号将来可能会派上用场。在Cat的这种新的横向解释中,有两种从一个对象到另一个对象的方法:使用函子或使用自然变换。然而,我们可以将函子箭头重新解释为一种特殊的自然变换:作用于该函子的恒等自然变换。所以你会经常看到这样的符号:
There is one more piece of notation that might come in handy in the future. In this new sideways interpretation of Cat there are two ways of getting from object to object: using a functor or using a natural transformation. We can, however, re-interpret the functor arrow as a special kind of natural transformation: the identity natural transformation acting on this functor. So you’ll often see this notation:
F ∘ αF ∘ α
其中 F 是从 D 到 E 的函子,α 是从 C 到 D 的两个函子之间的自然变换。由于您无法使用自然变换来组合函子,因此这被解释为恒等自然变换的水平组合α 后为1 F。
where F is a functor from D to E, and α is a natural transformation between two functors going from C to D. Since you can’t compose a functor with a natural transformation, this is interpreted as a horizontal composition of the identity natural transformation 1F after α.
相似地:
Similarly:
α ∘ Fα ∘ F
是 1 F之后 α 的水平合成。
is a horizontal composition of α after 1F.
本书的第一部分到此结束。我们已经学习了范畴论的基本词汇。您可能会将对象和范畴视为名词;以及作为动词的态射、函子和自然变换。态射连接对象,函子连接范畴,自然变换连接函子。
This concludes the first part of the book. We’ve learned the basic vocabulary of category theory. You may think of objects and categories as nouns; and morphisms, functors, and natural transformations as verbs. Morphisms connect objects, functors connect categories, natural transformations connect functors.
但我们也看到,在一个抽象层次上表现为动作的东西,在下一个层次上变成了对象。一组态射变成一个函数对象。作为一个对象,它可以是另一个态射的源或目标。这就是高阶函数背后的想法。
But we’ve also seen that, what appears as an action at one level of abstraction, becomes an object at the next level. A set of morphisms turns into a function object. As an object, it can be a source or a target of another morphism. That’s the idea behind higher order functions.
函子将对象映射到对象,因此我们可以将其用作类型构造函数或参数类型。函子还映射态射,因此它是一个高阶函数 - fmap。有一些简单的函子,例如Const、积和余积,可用于生成多种代数数据类型。函数类型也是函数类型,包括协变和逆变,并且可用于扩展代数数据类型。
A functor maps objects to objects, so we can use it as a type constructor, or a parametric type. A functor also maps morphisms, so it is a higher order function — fmap. There are some simple functors, like Const, product, and coproduct, that can be used to generate a large variety of algebraic data types. Function types are also functorial, both covariant and contravariant, and can be used to extend algebraic data types.
函子可以被视为函子范畴中的对象。因此,它们成为态射的源和目标:自然变换。自然变换是一种特殊类型的多态函数。
Functors may be looked upon as objects in the functor category. As such, they become sources and targets of morphisms: natural transformations. A natural transformation is a special type of polymorphic function.
Maybe定义从函子到列表函子的自然变换。证明其自然性条件。Maybe functor to the list functor. Prove the naturality condition for it.Reader ()在和 列表函子之间定义至少两个不同的自然变换。()有多少种不同的列表?Reader () and the list functor. How many different lists of () are there?Reader Bool使用和继续前面的练习Maybe。Reader Bool and Maybe.为不同函子之间的变换的相反自然条件创建一些测试用例Op。这是一种选择:
op :: Op Bool Int
op = Op (\x -> x > 0)
和
f :: String -> Int
f x = read xCreate a few test cases for the opposite naturality condition of transformations between different Op functors. Here’s one choice:
op :: Op Bool Int
op = Op (\x -> x > 0)
and
f :: String -> Int
f x = read x我要感谢 Gershom Bazerman 检查我的数学和逻辑,以及 André van Meulebrouck,他一直自愿提供编辑帮助。
I’d like to thank Gershom Bazerman for checking my math and logic, and André van Meulebrouck, who has been volunteering his editing help.
在本书的第一部分中,我认为范畴论和编程都与可组合性有关。在编程中,您不断分解问题,直到达到可以处理的详细程度,依次解决每个子问题,并自下而上重新组合解决方案。粗略地说,有两种方法:告诉计算机要做什么,或者告诉计算机如何做。一种称为声明式,另一种称为命令式。
In the first part of the book I argued that both category theory and programming are about composability. In programming, you keep decomposing a problem until you reach the level of detail that you can deal with, solve each subproblem in turn, and re-compose the solutions bottom-up. There are, roughly speaking, two ways of doing it: by telling the computer what to do, or by telling it how to do it. One is called declarative and the other imperative.
即使在最基本的层面上你也可以看到这一点。组合本身可以声明性地定义;如,是afterh的组合:gf
You can see this even at the most basic level. Composition itself may be defined declaratively; as in, h is a composite of g after f:
h = g . fh = g . f
或势在必行;例如,f首先调用,记住该调用的结果,然后g使用结果调用:
or imperatively; as in, call f first, remember the result of that call, then call g with the result:
h x = let y = f x
in g yh x = let y = f x
in g y
程序的命令式版本通常被描述为按时间排序的一系列操作。g特别是,在执行完成之前不能发生对 的调用f。至少,这是概念图——在惰性语言中,通过按需调用参数传递,实际执行可能会有所不同。
The imperative version of a program is usually described as a sequence of actions ordered in time. In particular, the call to g cannot happen before the execution of f completes. At least, that’s the conceptual picture — in a lazy language, with call-by-need argument passing, the actual execution may proceed differently.
事实上,根据编译器的聪明程度,声明性代码和命令性代码的执行方式可能几乎没有区别。但这两种方法在我们解决问题的方式以及结果代码的可维护性和可测试性方面存在差异,有时甚至是巨大的差异。
In fact, depending on the cleverness of the compiler, there may be little or no difference between how declarative and imperative code is executed. But the two methodologies differ, sometimes drastically, in the way we approach problem solving and in the maintainability and testability of the resulting code.
主要问题是:当面对问题时,我们是否总是可以在声明式和命令式解决方法之间做出选择?而且,如果有一个声明性解决方案,它总是可以翻译成计算机代码吗?这个问题的答案远非显而易见,如果我们能找到它,我们可能会彻底改变我们对宇宙的理解。
The main question is: when faced with a problem, do we always have the choice between a declarative and imperative approaches to solving it? And, if there is a declarative solution, can it always be translated into computer code? The answer to this question is far from obvious and, if we could find it, we would probably revolutionize our understanding of the universe.
让我详细说明一下。物理学中也存在类似的二元性,它要么指向一些深层的基本原理,要么告诉我们一些关于我们的思维如何运作的信息。理查德·费曼 (Richard Feynman) 在他自己的量子电动力学研究中提到了这种二元性作为灵感。
Let me elaborate. There is a similar duality in physics, which either points at some deep underlying principle, or tells us something about how our minds work. Richard Feynman mentions this duality as an inspiration in his own work on quantum electrodynamics.
大多数物理定律有两种表达形式。人们使用局部的或无穷小的考虑因素。我们观察一个小邻域周围系统的状态,并预测它在下一瞬间将如何演变。这通常使用微分方程来表达,微分方程必须在一段时间内积分或求和。
There are two forms of expressing most laws of physics. One uses local, or infinitesimal, considerations. We look at the state of a system around a small neighborhood, and predict how it will evolve within the next instant of time. This is usually expressed using differential equations that have to be integrated, or summed up, over a period of time.
请注意这种方法与命令式思维有何相似之处:我们通过遵循一系列小步骤来达到最终解决方案,每个步骤都取决于前一个步骤的结果。事实上,物理系统的计算机模拟通常是通过将微分方程转化为差分方程并迭代它们来实现的。这就是小行星游戏中宇宙飞船的动画方式。在每个时间步,宇宙飞船的位置都会通过添加一个小的增量来改变,该增量是通过将其速度乘以时间增量来计算的。反过来,速度会发生与加速度成比例的小增量的变化,加速度由力除以质量得出。
Notice how this approach resembles imperative thinking: we reach the final solution by following a sequence of small steps, each depending on the result of the previous one. In fact, computer simulations of physical systems are routinely implemented by turning differential equations into difference equations and iterating them. This is how spaceships are animated in the asteroids game. At each time step, the position of a spaceship is changed by adding a small increment, which is calculated by multiplying its velocity by the time delta. The velocity, in turn, is changed by a small increment proportional to acceleration, which is given by force divided by mass.
这些是与牛顿运动定律相对应的微分方程的直接编码:
These are the direct encodings of the differential equations corresponding to Newton’s laws of motion:
F = m dv/dt
v = dx/dtF = m dv/dt
v = dx/dt
类似的方法可以应用于更复杂的问题,例如使用麦克斯韦方程组的电磁场传播,甚至使用晶格 QCD(量子色动力学)研究质子内的夸克和胶子的行为。
Similar methods may be applied to more complex problems, like the propagation of electromagnetic fields using Maxwell’s equations, or even the behavior of quarks and gluons inside a proton using lattice QCD (quantum chromodynamics).
这种局部思维与数字计算机的使用所鼓励的空间和时间的离散化相结合,在斯蒂芬·沃尔夫勒姆将整个宇宙的复杂性降低到细胞自动机系统的英勇尝试中得到了极端的表达。
This local thinking combined with discretization of space and time that is encouraged by the use of digital computers found its extreme expression in the heroic attempt by Stephen Wolfram to reduce the complexity of the whole universe to a system of cellular automata.
另一种方法是全球性的。我们查看系统的初始状态和最终状态,并通过最小化某个函数来计算连接它们的轨迹。最简单的例子是费马最短时间原理。它指出光线沿着最小化其飞行时间的路径传播。特别是,在没有反射或折射物体的情况下,从A点到B点的光线将走最短路径,即一条直线。但光在致密(透明)材料(如水或玻璃)中传播速度较慢。所以如果你选择起点在空中,终点在水下,更有利于光在空中传播更长的时间,然后在水中抄近路。最短时间的路径使光线在空气和水的边界发生折射,从而产生斯涅尔折射定律:
The other approach is global. We look at the initial and the final state of the system, and calculate a trajectory that connects them by minimizing a certain functional. The simplest example is the Fermat’s principle of least time. It states that light rays propagate along paths that minimize their flight time. In particular, in the absence of reflecting or refracting objects, a light ray from point A to point B will take the shortest path, which is a straight line. But light propagates slower in dense (transparent) materials, like water or glass. So if you pick the starting point in the air, and the ending point under water, it’s more advantageous for light to travel longer in the air and then take a shortcut through water. The path of minimum time makes the ray refract at the boundary of air and water, resulting in Snell’s law of refraction:
sin θ1 / sin θ2 = v1 / v2sin θ1 / sin θ2 = v1 / v2
其中v1是空气中的光速,v2是水中的光速。
where v1 is the speed of light in the air and v2 is the speed of light in the water.
所有经典力学都可以从最小作用原理推导出来。可以通过积分拉格朗日来计算任何轨迹的作用,拉格朗日是动能和势能之间的差值(注意:这是差值,而不是总和 - 总和将是总能量)。当你发射迫击炮击中特定目标时,射弹将首先上升,那里由于重力而产生的势能较高,并花一些时间在那里对行动产生负面影响。它还会在抛物线顶部减速,以最小化动能。然后它会加速快速穿过低势能区域。
All of classical mechanics can be derived from the principle of least action. The action can be calculated for any trajectory by integrating the Lagrangian, which is the difference between kinetic and potential energy (notice: it’s the difference, not the sum — the sum would be the total energy). When you fire a mortar to hit a given target, the projectile will first go up, where the potential energy due to gravity is higher, and spend some time there racking up negative contribution to the action. It will also slow down at the top of the parabola, to minimize kinetic energy. Then it will speed up to go quickly through the area of low potential energy.
费曼最大的贡献是认识到最小作用原理可以推广到量子力学。再次,问题是根据初始状态和最终状态来表述的。这些状态之间的费曼路径积分用于计算转移概率。
Feynman’s greatest contribution was to realize that the principle of least action can be generalized to quantum mechanics. There, again, the problem is formulated in terms of initial state and final state. The Feynman path integral between those states is used to calculate the probability of transition.
关键是,我们描述物理定律的方式存在一种奇怪的、无法解释的二元性。我们可以使用本地图片,其中事物按顺序且以小增量发生。或者我们可以使用全局图,我们在其中声明初始条件和最终条件,并且两者之间的所有内容都将遵循。
The point is that there is a curious unexplained duality in the way we can describe the laws of physics. We can use the local picture, in which things happen sequentially and in small increments. Or we can use the global picture, where we declare the initial and final conditions, and everything in between just follows.
全局方法也可以用于编程,例如在实现光线追踪时。我们声明眼睛的位置和光源的位置,并找出连接它们的光线可能采取的路径。我们没有明确地最小化每条光线的飞行时间,但我们确实使用斯涅耳定律和反射几何来达到相同的效果。
The global approach can be also used in programming, for instance when implementing ray tracing. We declare the position of the eye and the positions of light sources, and figure out the paths that the light rays may take to connect them. We don’t explicitly minimize the time of flight for each ray, but we do use Snell’s law and the geometry of reflection to the same effect.
本地方法和全球方法之间最大的区别在于它们对空间的处理,更重要的是对时间的处理。局部方法拥抱此时此地的即时满足,而全局方法则采取长期静态的观点,就好像未来已经注定,而我们只是在分析某个永恒宇宙的属性。
The biggest difference between the local and the global approach is in their treatment of space and, more importantly, time. The local approach embraces the immediate gratification of here and now, whereas the global approach takes a long-term static view, as if the future had been preordained, and we were only analyzing the properties of some eternal universe.
没有什么比用户交互的函数响应式编程方法更好地说明这一点了。FRP 不是为每个可能的用户操作编写单独的处理程序(所有操作都可以访问某些共享的可变状态),而是将外部事件视为无限列表,并对其应用一系列转换。从概念上讲,我们所有未来行动的列表都在那里,可作为我们程序的输入数据。从程序的角度来看,π 的数字列表、伪随机数列表或通过计算机硬件传输的鼠标位置列表之间没有区别。在每种情况下,如果你想获得第 n 个项目,你必须首先遍历前 n-1 个项目。当应用于时间事件时,我们将此属性称为因果关系。
Nowhere is it better illustrated than in the Functional Reactive Programming approach to user interaction. Instead of writing separate handlers for every possible user action, all having access to some shared mutable state, FRP treats external events as an infinite list, and applies a series of transformations to it. Conceptually, the list of all our future actions is there, available as the input data to our program. From a program’s perspective there’s no difference between the list of digits of π, a list of pseudo-random numbers, or a list of mouse positions coming through computer hardware. In each case, if you want to get the nth item, you have to first go through the first n-1 items. When applied to temporal events, we call this property causality.
那么它和范畴论有什么关系呢?我认为范畴论鼓励全局方法,因此支持声明式编程。首先,与微积分不同,它没有内置的距离、邻域或时间概念。我们拥有的只是抽象对象以及它们之间的抽象联系。如果你可以通过一系列步骤从 A 到达 B,那么你也可以一跃到达那里。此外,范畴论的主要工具是普遍构造,这是全局方法的缩影。我们已经在实践中看到了它,例如,在分类产品的定义中。它是通过指定其属性来完成的——这是一种非常声明性的方法。它是一个配备有两个投影的对象,并且它是最好的此类对象 - 它优化了某个属性:分解其他此类对象的投影的属性。
So what does it have to do with category theory? I will argue that category theory encourages a global approach and therefore supports declarative programming. First of all, unlike calculus, it has no built-in notion of distance, or neighborhood, or time. All we have is abstract objects and abstract connections between them. If you can get from A to B through a series of steps, you can also get there in one leap. Moreover, the major tool of category theory is the universal construction, which is the epitome of a global approach. We’ve seen it in action, for instance, in the definition of the categorical product. It was done by specifying its properties — a very declarative approach. It’s an object equipped with two projections, and it’s the best such object — it optimizes a certain property: the property of factorizing the projections of other such objects.
将此与费马的最短时间原理或最少作用原理进行比较。
Compare this with Fermat’s principle of minimum time, or the principle of least action.
相反,将此与笛卡尔积的传统定义进行对比,后者更为必要。您描述如何通过从一组中选取一个元素并从另一组中选取另一个元素来创建产品的元素。这是创建一对的秘诀。还有一个用于拆卸一对。
Conversely, contrast this with the traditional definition of a cartesian product, which is much more imperative. You describe how to create an element of the product by picking one element from one set and another element from another set. It’s a recipe for creating a pair. And there’s another for disassembling a pair.
在几乎每种编程语言中,包括像 Haskell 这样的函数式语言,积类型、余积类型和函数类型都是内置的,而不是由通用结构定义的;尽管已经尝试创建分类编程语言(例如,参见Tatsuya Hagino 的论文)。
In almost every programming language, including functional languages like Haskell, product types, coproduct types, and function types are built in, rather than being defined by universal constructions; although there have been attempts at creating categorical programming languages (see, e.g., Tatsuya Hagino’s thesis).
无论是否直接使用,分类定义都会证明预先存在的编程结构的合理性,并产生新的结构。最重要的是,范畴论提供了一种在声明级别推理计算机程序的元语言。它还鼓励在将问题规范转化为代码之前对其进行推理。
Whether used directly or not, categorical definitions justify pre-existing programming constructs, and give rise to new ones. Most importantly, category theory provides a meta-language for reasoning about computer programs at a declarative level. It also encourages reasoning about problem specification before it is cast into code.
我要感谢 Gershom Bazerman 检查我的数学和逻辑,以及 André van Meulebrouck,他一直自愿提供编辑帮助。
I’d like to thank Gershom Bazerman for checking my math and logic, and André van Meulebrouck, who has been volunteering his editing help.
在范畴论中,一切似乎都相互关联,一切都可以从多个角度来看待。以产品的通用结构为例。现在我们对函子和自然变换有了更多的了解,我们可以简化并可能概括它吗?让我们尝试一下。
It seems like in category theory everything is related to everything and everything can be viewed from many angles. Take for instance the universal construction of the product. Now that we know more about functors and natural transformations, can we simplify and, possibly, generalize it? Let us try.
产品的构造从选择两个对象a和开始b,我们要构造其产品。但是选择对象意味着什么呢?我们可以用更明确的术语重新表述这一行动吗?两个物体形成一个图案——一个非常简单的图案。我们可以将这种模式抽象为一个范畴——一个非常简单的范畴,但仍然是一个范畴。我们将这个范畴称为2。它只包含两个对象 1 和 2,除了两个强制恒等式之外没有态射。现在我们可以将C中两个对象的选择重新表述为将函子 D 从范畴2定义为C的行为。函子将对象映射到对象,因此它的图像只是两个对象(或者它可能是一个,如果函子折叠对象,这也很好)。它还映射态射——在这种情况下,它只是将恒等态射映射到恒等态射。
The construction of a product starts with the selection of two objects a and b, whose product we want to construct. But what does it mean to select objects? Can we rephrase this action in more categorical terms? Two objects form a pattern — a very simple pattern. We can abstract this pattern into a category — a very simple category, but a category nevertheless. It’s a category that we’ll call 2. It contains just two objects, 1 and 2, and no morphisms other than the two obligatory identities. Now we can rephrase the selection of two objects in C as the act of defining a functor D from the category 2 to C. A functor maps objects to objects, so its image is just two objects (or it could be one, if the functor collapses objects, which is fine too). It also maps morphisms — in this case it simply maps identity morphisms to identity morphisms.
这种方法的优点在于它建立在绝对概念的基础上,避免了直接取自我们祖先狩猎采集词汇的“选择对象”等不精确的描述。顺便说一句,它也很容易推广,因为没有什么可以阻止我们使用比2更复杂的范畴来定义我们的模式。
What’s great about this approach is that it builds on categorical notions, eschewing the imprecise descriptions like “selecting objects,” taken straight from the hunter-gatherer lexicon of our ancestors. And, incidentally, it is also easily generalized, because nothing can stop us from using categories more complex than 2 to define our patterns.
但让我们继续吧。产品定义的下一步是选择候选对象c。在这里,我们再次可以根据单例范畴中的函子来重新表述选择。事实上,如果我们使用 Kan 扩展,那将是正确的做法。但由于我们还没有准备好 Kan 扩展,所以我们可以使用另一个技巧:从相同范畴2到C的常量函子 Δ 。Cc中的选择可以通过 Δ c来完成。请记住, Δ c将所有对象映射到并将所有态射映射到。cidc
But let’s continue. The next step in the definition of a product is the selection of the candidate object c. Here again, we could rephrase the selection in terms of a functor from a singleton category. And indeed, if we were using Kan extensions, that would be the right thing to do. But since we are not ready for Kan extensions yet, there is another trick we can use: a constant functor Δ from the same category 2 to C. The selection of c in C can be done with Δc. Remember, Δc maps all objects into c and all morphisms into idc.
现在我们有两个函子,Δ c和 D 在2和C之间,所以询问它们之间的自然变换是很自然的。由于2中只有两个对象,因此自然变换将具有两个分量。2中的对象 1c由 Δ c映射到,由 D 映射到。因此1 处Δ ca和 D之间的自然变换的分量是从到 的态射。我们可以这样称呼它。类似地,第二个分量是从到 的态射——D 下对象 2 in 2的图像。但是这些与我们在乘积的原始定义中使用的两个投影完全相同。因此,我们可以不讨论选择对象和投影,而只讨论选择函子和自然变换。碰巧的是,在这个简单的情况下,我们的变换的自然性条件很容易得到满足,因为2中不存在态射(除了恒等式)。capqcb
Now we have two functors, Δc and D going between 2 and C so it’s only natural to ask about natural transformations between them. Since there are only two objects in 2, a natural transformation will have two components. Object 1 in 2 is mapped to c by Δc and to a by D. So the component of a natural transformation between Δc and D at 1 is a morphism from c to a. We can call it p. Similarly, the second component is a morphism q from c to b — the image of the object 2 in 2 under D. But these are exactly like the two projections we used in our original definition of the product. So instead of talking about selecting objects and projections, we can just talk about picking functors and natural transformations. It so happens that in this simple case the naturality condition for our transformation is trivially satisfied, because there are no morphisms (other than the identities) in 2.
将此构造推广到2以外的范畴(例如,包含非平凡态射的范畴)将为 Δ c和 D 之间的变换施加自然条件。我们将这种变换称为锥体,因为 Δ 的图像是圆锥体/金字塔的顶点,其侧面由自然变换的分量形成。D 的图像构成了圆锥体的底面。
A generalization of this construction to categories other than 2 — ones that, for instance, contain non-trivial morphisms — will impose naturality conditions on the transformation between Δc and D. We call such transformation a cone, because the image of Δ is the apex of a cone/pyramid whose sides are formed by the components of the natural transformation. The image of D forms the base of the cone.
一般来说,要构建圆锥体,我们从定义模式的范畴I开始。这是一个很小的、通常是有限的范畴。我们从I到C选择一个函子 D并将其(或其图像)称为图。我们选择Cc中的一些作为圆锥体的顶点。我们用它来定义从I到C的常数函子 Δ c。从 Δ c到 D的自然变换就是我们的锥体。对于有限的I,它只是连接到图的一堆态射: I在 D 下的图像。c
In general, to build a cone, we start with a category I that defines the pattern. It’s a small, often finite category. We pick a functor D from I to C and call it (or its image) a diagram. We pick some c in C as the apex of our cone. We use it to define the constant functor Δc from I to C. A natural transformation from Δc to D is then our cone. For a finite I it’s just a bunch of morphisms connecting c to the diagram: the image of I under D.
自然性要求该图中的所有三角形(金字塔的壁)都是可交换的。事实上,f在I中采用任何态射。函子 D 将其映射到CD f中的态射,该态射形成某个三角形的底。常数函子 Δ c映射到 上的恒等态射。Δ 将态射的两端挤压成一个物体,自然性正方形变成了通勤三角形。该三角形的两条臂是自然变换的组成部分。fc
Naturality requires that all triangles (the walls of the pyramid) in this diagram commute. Indeed, take any morphism f in I. The functor D maps it to a morphism D f in C, a morphism that forms the base of some triangle. The constant functor Δc maps f to the identity morphism on c. Δ squishes the two ends of the morphism into one object, and the naturality square becomes a commuting triangle. The two arms of this triangle are the components of the natural transformation.
所以这是一个圆锥体。我们感兴趣的是通用锥体——就像我们为产品的定义选择了一个通用对象一样。
So that’s one cone. What we are interested in is the universal cone — just like we picked a universal object for our definition of a product.
有很多方法可以解决这个问题。例如,我们可以根据给定的函子 D 定义圆锥体的范畴。该范畴中的对象是圆锥体。然而,并非Cc中的每个物体都 可以是圆锥体的顶点,因为 Δ c和 D之间可能不存在自然变换。
There are many ways to go about it. For instance, we may define a category of cones based on a given functor D. Objects in that category are cones. Not every object c in C can be an apex of a cone, though, because there may be no natural transformation between Δc and D.
为了使其成为一个范畴,我们还必须定义锥体之间的态射。这些将完全由它们顶点之间的态射决定。但并非任何态射都可以。请记住,在我们构建产品时,我们施加了这样的条件:候选对象(顶点)之间的态射必须是投影的公因子。例如:
To make it a category, we also have to define morphisms between cones. These would be fully determined by morphisms between their apexes. But not just any morphism will do. Remember that, in our construction of the product, we imposed the condition that the morphisms between candidate objects (the apexes) must be common factors for the projections. For instance:
p' = p . m
q' = q . mp' = p . m
q' = q . m
在一般情况下,该条件转化为这样的条件:其一侧为因式分解态射的三角形全部交换。
This condition translates, in the general case, to the condition that the triangles whose one side is the factorizing morphism all commute.
连接两个锥体的交换三角形,具有因式分解态射h (这里,下面的锥体是通用锥体,Lim D其顶点)。
The commuting triangle connecting two cones, with the factorizing morphism h (here, the lower cone is the universal one, with Lim D as its apex).
我们将把那些分解态射作为我们锥范畴中的态射。很容易检查这些态射是否确实组成,并且恒等态射也是因式分解态射。因此视锥细胞形成一个范畴。
We’ll take those factorizing morphisms as the morphisms in our category of cones. It’s easy to check that those morphisms indeed compose, and that the identity morphism is a factorizing morphism as well. Cones therefore form a category.
现在我们可以将通用圆锥体定义为圆锥体范畴中的终端对象。终端对象的定义指出,从任何其他对象到该对象都存在唯一的态射。在我们的例子中,这意味着从任何其他锥体的顶点到通用锥体的顶点存在独特的因式分解态射。我们将这个通用锥体称为图 D 的极限(在文献中,您经常会在符号下Lim D看到一个指向I 的Lim左箭头)。通常,作为简写,我们将该圆锥体的顶点称为极限(或极限物体)。
Now we can define the universal cone as the terminal object in the category of cones. The definition of the terminal object states that there is a unique morphism from any other object to that object. In our case it means that there is a unique factorizing morphism from the apex of any other cone to the apex of the universal cone. We call this universal cone the limit of the diagram D, Lim D (in the literature, you’ll often see a left arrow pointing towards I under the Lim sign). Often, as a shorthand, we call the apex of this cone the limit (or the limit object).
直觉是,极限体现了单个对象中整个图的属性。例如,我们的双对象图的极限是两个对象的乘积。该乘积(连同两个投影)包含有关两个对象的信息。通用意味着它没有无关的垃圾。
The intuition is that the limit embodies the properties of the whole diagram in a single object. For instance, the limit of our two-object diagram is the product of two objects. The product (together with the two projections) contains the information about both objects. And being universal means that it has no extraneous junk.
这个极限的定义仍然有一些不令人满意的地方。我的意思是,它是可行的,但是对于连接任意两个圆锥的三角形,我们仍然有这种交换性条件。如果我们能用一些自然条件来代替它,那就优雅多了。但如何呢?
There is still something unsatisfying about this definition of a limit. I mean, it’s workable, but we still have this commutativity condition for the triangles that are linking any two cones. It would be so much more elegant if we could replace it with some naturality condition. But how?
我们不再处理一个圆锥体,而是处理整个圆锥体集合(实际上是一个范畴)。如果极限存在(并且——让我们明确一点——不能保证这一点),那么这些锥体之一就是万能锥体。对于每个其他锥体,我们都有一个独特的因式分解态射,将其顶点(我们称之为 )映射c到通用锥体的顶点(我们将其命名为Lim D)。(事实上,我可以跳过“其他”这个词,因为恒等态射将通用锥体映射到自身,并且它通过自身进行因式分解。)让我重复一下重要的部分:给定任何锥体,都有一个特殊的态射种类。我们有一个锥体到特殊态射的映射,并且它是一对一的映射。
We are no longer dealing with one cone but with a whole collection (in fact, a category) of cones. If the limit exists (and — let’s make it clear — there’s no guarantee of that), one of those cones is the universal cone. For every other cone we have a unique factorizing morphism that maps its apex, let’s call it c, to the apex of the universal cone, which we named Lim D. (In fact, I can skip the word “other,” because the identity morphism maps the universal cone to itself and it trivially factorizes through itself.) Let me repeat the important part: given any cone, there is a unique morphism of a special kind. We have a mapping of cones to special morphisms, and it’s a one-to-one mapping.
这种特殊的态射是 hom-set 的成员C(c, Lim D)。这个 hom-set 的其他成员就没那么幸运了,因为他们没有分解视锥细胞的映射。我们想要的是能够为每个c,从集合中挑选一个态射C(c, Lim D)——满足特定交换性条件的态射。这听起来像是定义自然变换吗?确实如此!
This special morphism is a member of the hom-set C(c, Lim D). The other members of this hom-set are less fortunate, in the sense that they don’t factorize the mapping of cones. What we want is to be able to pick, for each c, one morphism from the set C(c, Lim D) — a morphism that satisfies the particular commutativity condition. Does that sound like defining a natural transformation? It most certainly does!
但是与此变换相关的函子是什么?
But what are the functors that are related by this transformation?
c一个函子是到集合的映射C(c, Lim D)。它是从C到Set 的函子——它将对象映射到集合。事实上它是一个逆变函子。f下面是我们如何定义它对态射的作用:让我们从c'to进行态射c:
One functor is the mapping of c to the set C(c, Lim D). It’s a functor from C to Set — it maps objects to sets. In fact it’s a contravariant functor. Here’s how we define its action on morphisms: Let’s take a morphism f from c' to c:
f :: c' -> cf :: c' -> c
我们的函子映射c'到集合C(c', Lim D)。为了定义这个函子在 上的动作f(换句话说,提升),我们必须定义和f之间相应的映射。因此,让我们选择的一个元素,看看是否可以将其映射到 的某个元素。hom-set 的一个元素是态射,所以我们有:C(c, Lim D)C(c', Lim D)uC(c, Lim D)C(c', Lim D)
Our functor maps c' to the set C(c', Lim D). To define the action of this functor on f (in other words, to lift f), we have to define the corresponding mapping between C(c, Lim D) and C(c', Lim D). So let’s pick one element u of C(c, Lim D) and see if we can map it to some element of C(c', Lim D). An element of a hom-set is a morphism, so we have:
u :: c -> Lim Du :: c -> Lim D
我们可以预组合u得到f:
We can precompose u with f to get:
u . f :: c' -> Lim Du . f :: c' -> Lim D
这是一个元素C(c', Lim D)——所以事实上,我们已经找到了态射的映射:
And that’s an element of C(c', Lim D)— so indeed, we have found a mapping of morphisms:
contramap :: (c' -> c) -> (c -> Lim D) -> (c' -> Lim D)
contramap f u = u . fcontramap :: (c' -> c) -> (c -> Lim D) -> (c' -> Lim D)
contramap f u = u . f
注意逆变函子的顺序c和c'特征的反转。
Notice the inversion in the order of c and c' characteristic of a contravariant functor.
为了定义自然变换,我们需要另一个函子,它也是从C到Set的映射。但这一次我们将考虑一组锥体。锥体只是自然变换,因此我们正在研究自然变换的集合Nat(Δc, D)。c从这组特定的自然变换到的映射是一个(逆变)函子。我们怎样才能证明这一点?再次,让我们定义它对态射的作用:
To define a natural transformation, we need another functor that’s also a mapping from C to Set. But this time we’ll consider a set of cones. Cones are just natural transformations, so we are looking at the set of natural transformations Nat(Δc, D). The mapping from c to this particular set of natural transformations is a (contravariant) functor. How can we show that? Again, let’s define its action on a morphism:
f :: c' -> cf :: c' -> c
的提升应该是从I到Cf的两个函子之间的自然变换的映射:
The lifting of f should be a mapping of natural transformations between two functors that go from I to C:
Nat(Δc, D) -> Nat(Δc', D)Nat(Δc, D) -> Nat(Δc', D)
我们如何绘制自然转变图?每个自然变换都是态射的选择 - 它的组成部分 - I的每个元素一个态射。某个 α ( 的成员Nat(Δc, D)) 在a(I中的一个对象)处的一个分量是态射:
How do we map natural transformations? Every natural transformation is a selection of morphisms — its components — one morphism per element of I. A component of some α (a member of Nat(Δc, D)) at a (an object in I) is a morphism:
αa :: Δca -> D aαa :: Δca -> D a
或者,使用常数函子 Δ 的定义,
or, using the definition of the constant functor Δ,
αa :: c -> D aαa :: c -> D a
给定f和 α,我们必须构造一个 β,它是 的成员Nat(Δc', D)。它的分量 ata应该是一个态射:
Given f and α, we have to construct a β, a member of Nat(Δc', D). Its component at a should be a morphism:
βa :: c' -> D aβa :: c' -> D a
通过将其预先组合,我们可以轻松地从前者获得后者f:
We can easily get the latter from the former by precomposing it with f:
βa = αa . fβa = αa . f
相对容易地证明这些成分确实构成了自然的转变。
It’s relatively easy to show that those components indeed add up to a natural transformation.
给定我们的态射f,我们就在两个自然变换之间建立了组件方面的映射。该映射定义了contramap函子:
Given our morphism f, we have thus built a mapping between two natural transformations, component-wise. This mapping defines contramap for the functor:
c -> Nat(Δc, D)c -> Nat(Δc, D)
我刚刚所做的就是向您展示我们有两个从C到Set的(逆变)函子。我没有做出任何假设——这些函子总是存在的。
What I have just done is to show you that we have two (contravariant) functors from C to Set. I haven’t made any assumptions — these functors always exist.
顺便说一句,第一个函子在范畴论中起着重要作用,当我们谈论米田引理时我们会再次看到它。从任何范畴C到Set 的逆变函子都有一个名称:它们被称为“presheaves”。这称为可表示预束。第二个函子也是预轴。
Incidentally, the first of these functors plays an important role in category theory, and we’ll see it again when we talk about Yoneda’s lemma. There is a name for contravariant functors from any category C to Set: they are called “presheaves.” This one is called a representable presheaf. The second functor is also a presheaf.
现在我们有了两个函子,我们可以讨论它们之间的自然变换。言归正传,结论如下:D从I到C 的函子有极限Lim D当且仅当我刚刚定义的两个函子之间存在自然同构:
Now that we have two functors, we can talk about natural transformations between them. So without further ado, here’s the conclusion: A functor D from I to C has a limit Lim D if and only if there is a natural isomorphism between the two functors I have just defined:
C(c, Lim D) ≃ Nat(Δc, D)C(c, Lim D) ≃ Nat(Δc, D)
让我提醒您什么是自然同构。这是一种自然变换,其每个分量都是同构,即可逆态射。
Let me remind you what a natural isomorphism is. It’s a natural transformation whose every component is an isomorphism, that is to say an invertible morphism.
我不打算详细说明这个陈述的证明。该过程即使不乏味,也非常简单。在处理自然变换时,您通常关注组件,即态射。在这种情况下,由于两个函子的目标都是Set,因此自然同构的组件将是函数。这些是高阶函数,因为它们从 hom 集到自然变换集。同样,您可以通过考虑函数对其参数的作用来分析函数:这里的参数将是一个态射( 的成员),C(c, Lim D)结果将是一个自然变换( 的成员)Nat(Δc, D),或者我们所说的锥体。反过来,这种自然变换有其自己的组成部分,即态射。所以它一直都是态射,如果你能跟踪它们,你就可以证明这个命题。
I’m not going to go through the proof of this statement. The procedure is pretty straightforward if not tedious. When dealing with natural transformations, you usually focus on components, which are morphisms. In this case, since the target of both functors is Set, the components of the natural isomorphism will be functions. These are higher order functions, because they go from the hom-set to the set of natural transformations. Again, you can analyze a function by considering what it does to its argument: here the argument will be a morphism — a member of C(c, Lim D) — and the result will be a natural transformation — a member of Nat(Δc, D), or what we have called a cone. This natural transformation, in turn, has its own components, which are morphisms. So it’s morphisms all the way down, and if you can keep track of them, you can prove the statement.
最重要的结果是,这种同构的自然性条件正是锥映射的交换性条件。
The most important result is that the naturality condition for this isomorphism is exactly the commutativity condition for the mapping of cones.
作为即将到来的吸引力的预览,让我提一下,该集合Nat(Δc, D)可以被认为是函子范畴中的 hom-set;因此,我们的自然同构将两个 hom 集联系起来,这指向一种更普遍的关系,称为附加关系。
As a preview of coming attractions, let me mention that the set Nat(Δc, D) can be thought of as a hom-set in the functor category; so our natural isomorphism relates two hom-sets, which points at an even more general relationship called an adjunction.
我们已经看到,分类乘积是由我们称为2 的简单范畴生成的图表的限制。
We’ve seen that the categorical product is a limit of a diagram generated by a simple category we called 2.
还有一个更简单的限制示例:终端对象。第一个冲动是认为单例范畴会导致最终对象,但事实比这更严峻:最终对象是由空范畴生成的限制。空范畴中的函子不选择任何对象,因此圆锥体缩小到仅顶点。通用锥体是唯一的顶点,它具有从任何其他顶点到它的独特态射。您将认识到这是终端对象的定义。
There is an even simpler example of a limit: the terminal object. The first impulse would be to think of a singleton category as leading to a terminal object, but the truth is even starker than that: the terminal object is a limit generated by an empty category. A functor from an empty category selects no object, so a cone shrinks to just the apex. The universal cone is the lone apex that has a unique morphism coming to it from any other apex. You will recognize this as the definition of the terminal object.
下一个有趣的限制称为均衡器。它是由二元范畴产生的极限,它们之间有两个平行态射(以及一如既往的恒等态射)。此范畴选择由两个对象和以及两个态射组成的C语言图:ab
The next interesting limit is called the equalizer. It’s a limit generated by a two-element category with two parallel morphisms going between them (and, as always, the identity morphisms). This category selects a diagram in C consisting of two objects, a and b, and two morphisms:
f :: a -> b
g :: a -> bf :: a -> b
g :: a -> b
要在此图上构建圆锥体,我们必须添加顶点c和两个投影:
To build a cone over this diagram, we have to add the apex, c and two projections:
p :: c -> a
q :: c -> bp :: c -> a
q :: c -> b
我们有两个必须交换的三角形:
We have two triangles that must commute:
q = f . p
q = g . pq = f . p
q = g . p
这告诉我们q是由这些方程之一唯一确定的,例如 ,q = f . p我们可以从图中省略它。所以我们只剩下一个条件:
This tells us that q is uniquely determined by one of these equations, say, q = f . p, and we can omit it from the picture. So we are left with just one condition:
f . p = g . pf . p = g . p
思考的方法是,如果我们将注意力限制在Set上,则函数的图像p会选择 的子集a。当限制于该子集时,函数f和g是相等的。
The way to think about it is that, if we restrict our attention to Set, the image of the function p selects a subset of a. When restricted to this subset, the functions f and g are equal.
例如,取为由坐标和a参数化的二维平面。取实线,并取:xyb
For instance, take a to be the two-dimensional plane parameterized by coordinates x and y. Take b to be the real line, and take:
f (x, y) = 2 * y + x
g (x, y) = y - xf (x, y) = 2 * y + x
g (x, y) = y - x
这两个函数的均衡器是实数集(顶点c)和函数:
The equalizer for these two functions is the set of real numbers (the apex, c) and the function:
p t = (t, (-2) * t)p t = (t, (-2) * t)
请注意,它(p t)在二维平面中定义了一条直线。沿着这条线,这两个功能是相等的。
Notice that (p t) defines a straight line in the two-dimensional plane. Along this line, the two functions are equal.
当然,还有其他集合c'和函数p'可能导致相等:
Of course, there are other sets c' and functions p' that may lead to the equality:
f . p' = g . p'f . p' = g . p'
但它们都是通过 独特地分解出来的p。例如,我们可以将单例集()作为c'函数:
but they all uniquely factor out through p. For instance, we can take the singleton set () as c' and the function:
p'() = (0, 0)p'() = (0, 0)
这是一个很好的锥体,因为f (0, 0) = g (0, 0). 但它并不通用,因为通过以下独特的因式分解h:
It’s a good cone, because f (0, 0) = g (0, 0). But it’s not universal, because of the unique factorization through h:
p' = p . hp' = p . h
和
with
h () = 0h () = 0
因此,均衡器可用于求解 类型的方程f x = g x。但它更为一般,因为它是根据对象和态射而不是代数来定义的。
An equalizer can thus be used to solve equations of the type f x = g x. But it’s much more general, because it’s defined in terms of objects and morphisms rather than algebraically.
求解方程的一个更普遍的想法体现在另一个限制中——回调。在这里,我们仍然有两个想要等同的态射,但这次它们的域不同。我们从形状的三对象范畴开始:1->2<-3。与该范畴对应的图由三个对象 、a、b和c以及两个态射组成:
An even more general idea of solving an equation is embodied in another limit — the pullback. Here, we still have two morphisms that we want to equate, but this time their domains are different. We start with a three-object category of the shape: 1->2<-3. The diagram corresponding to this category consists of three objects, a, b, and c, and two morphisms:
f :: a -> b
g :: c -> bf :: a -> b
g :: c -> b
该图通常称为cospan。
This diagram is often called a cospan.
在此图之上构建的圆锥由顶点 、d和三个态射组成:
A cone built on top of this diagram consists of the apex, d, and three morphisms:
p :: d -> a
q :: d -> c
r :: d -> bp :: d -> a
q :: d -> c
r :: d -> b
交换性条件告诉我们r完全由其他态射决定,并且可以从图中省略。所以我们只剩下以下条件:
Commutativity conditions tell us that r is completely determined by the other morphisms, and can be omitted from the picture. So we are only left with the following condition:
g . q = f . pg . q = f . p
回拉是这种形状的通用圆锥体。
A pullback is a universal cone of this shape.
同样,如果将焦点缩小到集合,则可以将对象视为d由元素对组成a,c对f第一个组件的作用等于g对第二个组件的作用。如果这仍然太笼统,请考虑特殊情况,其中g是常数函数g _ = 1.23(假设b是一组实数)。那么你就真正求解方程了:
Again, if you narrow your focus down to sets, you can think of the object d as consisting of pairs of elements from a and c for which f acting on the first component is equal to g acting on the second component. If this is still too general, consider the special case in which g is a constant function, say g _ = 1.23 (assuming that b is a set of real numbers). Then you are really solving the equation:
f x = 1.23f x = 1.23
在这种情况下, 的选择c是无关紧要的(只要它不是空集),因此我们可以将其视为单例集。例如,该集合a可以是三维向量和f向量长度的集合。那么回拉就是对的集合(v, ()),其中v是长度为 1.23 的向量(方程 的解sqrt (x2+y2+z2) = 1.23),()是单例集合的虚拟元素。
In this case, the choice of c is irrelevant (as long as it’s not an empty set), so we can take it to be a singleton set. The set a could, for instance, be the set of three-dimensional vectors, and f the vector length. Then the pullback is the set of pairs (v, ()), where v is a vector of length 1.23 (a solution to the equation sqrt (x2+y2+z2) = 1.23), and () is the dummy element of the singleton set.
但回调有更普遍的应用,在编程中也是如此。例如,将 C++ 类视为一个范畴,其中态射是将子类连接到超类的箭头。我们将继承视为传递属性,因此如果 C 继承自 B 并且 B 继承自 A,那么我们会说 C 继承自 A(毕竟,您可以将指针传递给 C,而需要指向 A 的指针)。另外,我们假设 C 继承自 C,因此每个类都有恒等箭头。通过这种方式,子类化与子类型化是一致的。C++ 还支持多重继承,因此您可以构造一个菱形继承图,其中两个类 B 和 C 继承自 A,第四个类 D 多重继承自 B 和 C。通常,D 会获得 A 的两个副本,这是很少可取的; 但您可以使用虚拟继承在 D 中仅拥有 A 的一份副本。
But pullbacks have more general applications, also in programming. For instance, consider C++ classes as a category in which morphism are arrows that connect subclasses to superclasses. We’ll consider inheritance a transitive property, so if C inherits from B and B inherits from A then we’ll say that C inherits from A (after all, you can pass a pointer to C where a pointer to A is expected). Also, we’ll assume that C inherits from C, so we have the identity arrow for every class. This way subclassing is aligned with subtyping. C++ also supports multiple inheritance, so you can construct a diamond inheritance diagram with two classes B and C inheriting from A, and a fourth class D multiply inheriting from B and C. Normally, D would get two copies of A, which is rarely desirable; but you can use virtual inheritance to have just one copy of A in D.
在此图中,D 为回调意味着什么?这意味着任何从 B 和 C 继承的类 E 也是 D 的子类。这在 C++ 中不能直接表达,其中子类型是名义上的(C++ 编译器不会推断这种类关系 - 它需要“鸭子打字”)。但我们可以跳出子类型关系,转而询问从 E 到 D 的强制转换是否安全。如果 D 是 B 和 C 的基本组合,没有额外的数据,也没有重写方法,那么这个转换将是安全的。当然,如果 B 和 C 的某些方法之间存在名称冲突,也不会出现回调。
What would it mean to have D be a pullback in this diagram? It would mean that any class E that multiply inherits from B and C is also a subclass of D. This is not directly expressible in C++, where subtyping is nominal (the C++ compiler wouldn’t infer this kind of class relationship — it would require “duck typing”). But we could go outside of the subtyping relationship and instead ask whether a cast from E to D would be safe or not. This cast would be safe if D were the bare-bone combination of B and C, with no additional data and no overriding of methods. And, of course, there would be no pullback if there is a name conflict between some methods of B and C.
在类型推断中还有更高级的回调用法。通常需要统一两个表达式的类型。例如,假设编译器想要推断函数的类型:
There’s also a more advanced use of a pullback in type inference. There is often a need to unify types of two expressions. For instance, suppose that the compiler wants to infer the type of a function:
twice f x = f (f x)twice f x = f (f x)
它将为所有变量和子表达式分配初步类型。特别是,它将分配:
It will assign preliminary types to all variables and sub-expressions. In particular, it will assign:
f :: t0
x :: t1
f x :: t2
f (f x) :: t3f :: t0
x :: t1
f x :: t2
f (f x) :: t3
由此可以推断出:
from which it will deduce that:
twice :: t0 -> t1 -> t3twice :: t0 -> t1 -> t3
它还会提出一组由函数应用规则产生的约束:
It will also come up with a set of constraints resulting from the rules of function application:
t0 = t1 -> t2 -- because f is applied to x
t0 = t2 -> t3 -- because f is applied to (f x)t0 = t1 -> t2 -- because f is applied to x
t0 = t2 -> t3 -- because f is applied to (f x)
这些约束必须通过查找一组类型(或类型变量)来统一,这些类型在替换两个表达式中的未知类型时会产生相同的类型。一种这样的替代是:
These constraints have to be unified by finding a set of types (or type variables) that, when substituted for the unknown types in both expressions, produce the same type. One such substitution is:
t1 = t2 = t3 = Int
twice :: (Int -> Int) -> Int -> Intt1 = t2 = t3 = Int
twice :: (Int -> Int) -> Int -> Int
但显然,这不是最通用的。最常见的替换是通过回调来实现的。我不会详细介绍,因为它们超出了本书的范围,但你可以说服自己,结果应该是:
but, obviously, it’s not the most general one. The most general substitution is obtained using a pullback. I won’t go into the details, because they are beyond the scope of this book, but you can convince yourself that the result should be:
twice :: (t -> t) -> t -> ttwice :: (t -> t) -> t -> t
带有t自由类型变量。
with t a free type variable.
就像范畴论中的所有构造一样,极限在相反的范畴中也有其双重形象。当你反转圆锥体中所有箭头的方向时,你会得到一个共圆锥体,其中通用的圆锥体称为共极限。请注意,反演也会影响因式分解态射,它现在从通用共锥流到任何其他共锥。
Just like all constructions in category theory, limits have their dual image in opposite categories. When you invert the direction of all arrows in a cone, you get a co-cone, and the universal one of those is called a colimit. Notice that the inversion also affects the factorizing morphism, which now flows from the universal co-cone to any other co-cone.
具有连接两个顶点的分解态射的 Cocone h。
Cocone with a factorizing morphism h connecting two apexes.
余极限的一个典型示例是余积,它对应于2生成的图表,2 是我们在乘积定义中使用的范畴。
A typical example of a colimit is a coproduct, which corresponds to the diagram generated by 2, the category we’ve used in the definition of the product.
乘积和余积都以不同的方式体现了一对对象的本质。
Both the product and the coproduct embody the essence of a pair of objects, each in a different way.
就像终端对象是一个极限一样,初始对象也是一个与基于空范畴的图对应的余极限。
Just like the terminal object was a limit, so the initial object is a colimit corresponding to the diagram based on an empty category.
回调的对偶称为推出。它基于一个称为跨度的图表,由范畴生成1<-2->3。
The dual of the pullback is called the pushout. It’s based on a diagram called a span, generated by the category 1<-2->3.
我之前说过,函子接近范畴连续映射的思想,因为它们永远不会破坏现有的连接(态射)。从范畴C到C'的连续函子 的实际定义包括函子保留极限的要求。通过简单地组合两个函子, C中的每个图都可以映射到C'中的图。的连续性条件表明,如果图有极限,则图也有极限,并且它等于。FDF ∘ DFDLim DF ∘ DF (Lim D)
I said previously that functors come close to the idea of continuous mappings of categories, in the sense that they never break existing connections (morphisms). The actual definition of a continuous functor F from a category C to C’ includes the requirement that the functor preserve limits. Every diagram D in C can be mapped to a diagram F ∘ D in C’ by simply composing two functors. The continuity condition for F states that, if the diagram D has a limit Lim D, then the diagram F ∘ D also has a limit, and it is equal to F (Lim D).
请注意,因为函子将态射映射到态射,将组合映射到组合,所以圆锥体的图像始终是圆锥体。通勤三角形始终映射到通勤三角形(函子保留组合)。对于因式态射也是如此:因式态射的图像也是因式态射。所以每个函子几乎都是连续的。可能出错的是唯一性条件。C'中的因式分解态射可能不是唯一的。C'中可能还存在C中没有的其他“更好的锥体” 。
Notice that, because functors map morphisms to morphisms, and compositions to compositions, an image of a cone is always a cone. A commuting triangle is always mapped to a commuting triangle (functors preserve composition). The same is true for the factorizing morphisms: the image of a factorizing morphism is also a factorizing morphism. So every functor is almost continuous. What may go wrong is the uniqueness condition. The factorizing morphism in C’ might not be unique. There may also be other “better cones” in C’ that were not available in C.
hom 函子是连续函子的一个示例。回想一下,hom 函子 ,C(a, b)在第一个变量中是逆变的,在第二个变量中是协变的。换句话说,它是一个函子:
A hom-functor is an example of a continuous functor. Recall that the hom-functor, C(a, b), is contravariant in the first variable and covariant in the second. In other words, it’s a functor:
Cop × C -> SetCop × C -> Set
当其第二个参数固定时, hom-set 函子(成为可表示的 presheaf)将C中的余极限映射到Set中的极限;当它的第一个参数固定时,它将限制映射到限制。
When its second argument is fixed, the hom-set functor (which becomes the representable presheaf) maps colimits in C to limits in Set; and when its first argument is fixed, it maps limits to limits.
在 Haskell 中,hom-functor 是任意两种类型到函数类型的映射,因此它只是一个参数化函数类型。当我们固定第二个参数时,假设为String,我们得到逆变函子:
In Haskell, a hom-functor is the mapping of any two types to a function type, so it’s just a parameterized function type. When we fix the second parameter, let’s say to String, we get the contravariant functor:
newtype ToString a = ToString (a -> String)
instance Contravariant ToString where
contramap f (ToString g) = ToString (g . f)newtype ToString a = ToString (a -> String)
instance Contravariant ToString where
contramap f (ToString g) = ToString (g . f)
连续性意味着当ToString应用于余极限(例如余积)时Either b c,它将产生极限;在本例中是两个函数类型的乘积:
Continuity means that when ToString is applied to a colimit, for instance a coproduct Either b c, it will produce a limit; in this case a product of two function types:
ToString (Either b c) ~ (b -> String, c -> String)ToString (Either b c) ~ (b -> String, c -> String)
事实上, 的任何函数都Either b c被实现为一个 case 语句,并且这两个 case 由一对函数提供服务。
Indeed, any function of Either b c is implemented as a case statement with the two cases being serviced by a pair of functions.
类似地,当我们修复 hom-set 的第一个参数时,我们得到了熟悉的 reader 函子。它的连续性意味着,例如,任何返回乘积的函数都相当于函数的乘积;尤其:
Similarly, when we fix the first argument of the hom-set, we get the familiar reader functor. Its continuity means that, for instance, any function returning a product is equivalent to a product of functions; in particular:
r -> (a, b) ~ (r -> a, r -> b)r -> (a, b) ~ (r -> a, r -> b)
我知道你在想什么:你不需要范畴论来解决这些问题。你是对的!尽管如此,我仍然感到惊讶的是,这样的结果可以从第一原理中推导出来,而不需要借助位和字节、处理器架构、编译器技术,甚至 lambda 演算。
I know what you’re thinking: You don’t need category theory to figure these things out. And you’re right! Still, I find it amazing that such results can be derived from first principles with no recourse to bits and bytes, processor architectures, compiler technologies, or even lambda calculus.
如果您好奇“极限”和“连续性”这两个名称的由来,它们是微积分中相应概念的概括。在微积分中,极限和连续性是根据开邻域来定义的。定义拓扑的开集形成一个范畴(偏序集)。
If you’re curious where the names “limit” and “continuity” come from, they are a generalization of the corresponding notions from calculus. In calculus limits and continuity are defined in terms of open neighborhoods. Open sets, which define topology, form a category (a poset).
Id :: C -> C是初始对象。Id :: C -> C is the initial object.我要感谢 Gershom Bazerman 检查我的数学和逻辑,以及 André van Meulebrouck,他一直自愿提供编辑帮助。
I’d like to thank Gershom Bazerman for checking my math and logic, and André van Meulebrouck, who has been volunteering his editing help.
幺半群是范畴论和编程中的一个重要概念。范畴对应于强类型语言,幺半群对应于非类型语言。这是因为在幺半群中,您可以组合任意两个箭头,就像在无类型语言中您可以组合任意两个函数一样(当然,在执行程序时可能会出现运行时错误)。
Monoids are an important concept in both category theory and in programming. Categories correspond to strongly typed languages, monoids to untyped languages. That’s because in a monoid you can compose any two arrows, just as in an untyped language you can compose any two functions (of course, you may end up with a runtime error when you execute your program).
我们已经看到,幺半群可以被描述为具有单个对象的范畴,其中所有逻辑都以态射组合规则进行编码。这种分类模型完全等同于更传统的集合论定义的幺半群,其中我们将集合中的两个元素“相乘”以获得第三个元素。这个“乘法”过程可以进一步分解为首先形成一对元素,然后用现有元素(它们的“乘积”)来识别这对元素。
We’ve seen that a monoid may be described as a category with a single object, where all logic is encoded in the rules of morphism composition. This categorical model is fully equivalent to the more traditional set-theoretical definition of a monoid, where we “multiply” two elements of a set to get a third element. This process of “multiplication” can be further dissected into first forming a pair of elements and then identifying this pair with an existing element — their “product.”
当我们放弃乘法的第二部分——用现有元素识别对时会发生什么?例如,我们可以从任意集合开始,形成所有可能的元素对,并将它们称为新元素。然后我们将这些新元素与所有可能的元素配对,依此类推。这是一个连锁反应——我们将永远不断地添加新元素。结果是一个无限集,几乎是一个幺半群。但幺半群还需要单位元素和结合律。没问题,我们可以添加一个特殊的单位元素并识别一些对 - 足以支持单位和结合律。
What happens when we forgo the second part of multiplication — the identification of pairs with existing elements? We can, for instance, start with an arbitrary set, form all possible pairs of elements, and call them new elements. Then we’ll pair these new elements with all possible elements, and so on. This is a chain reaction — we’ll keep adding new elements forever. The result, an infinite set, will be almost a monoid. But a monoid also needs a unit element and the law of associativity. No problem, we can add a special unit element and identify some of the pairs — just enough to support the unit and associativity laws.
让我们通过一个简单的例子来看看它是如何工作的。让我们从两个元素的集合开始{a, b}。我们将它们称为自由幺半群的生成器。首先,我们将添加一个特殊元素e作为单元。接下来我们将添加所有元素对并将它们称为“产品”。a和的乘积b将是对(a, b)。b和的乘积a将是对,与 的(b, a)乘积将是,与 的乘积将是。我们还可以用、、等组成对,但我们将用、等来标识它们。所以在这一轮中我们只会添加、和,并最终得到集合。aa(a, a)bb(b, b)e(a, e)(e, b)ab(a, a)(a, b)(b, a)(b, b){e, a, b, (a, a), (a, b), (b, a), (b, b)}
Let’s see how this works in a simple example. Let’s start with a set of two elements, {a, b}. We’ll call them the generators of the free monoid. First, we’ll add a special element e to serve as the unit. Next we’ll add all the pairs of elements and call them “products”. The product of a and b will be the pair (a, b). The product of b and a will be the pair (b, a), the product of a with a will be (a, a), the product of b with b will be (b, b). We can also form pairs with e, like (a, e), (e, b), etc., but we’ll identify them with a, b, etc. So in this round we’ll only add (a, a), (a, b) and (b, a) and (b, b), and end up with the set {e, a, b, (a, a), (a, b), (b, a), (b, b)}.
在下一轮中,我们将继续添加诸如:(a, (a, b))、((a, b), a)等元素。此时,我们必须确保结合性成立,因此我们将识别(a, (b, a))等((a, b), a)。换句话说,我们不需要内部括号。
In the next round we’ll keep adding elements like: (a, (a, b)), ((a, b), a), etc. At this point we’ll have to make sure that associativity holds, so we’ll identify (a, (b, a)) with ((a, b), a), etc. In other words, we won’t be needing internal parentheses.
您可以猜测此过程的最终结果是什么:我们将创建as 和bs 的所有可能列表。事实上,如果我们表示e为一个空列表,我们可以看到我们的“乘法”只不过是列表串联。
You can guess what the final result of this process will be: we’ll create all possible lists of as and bs. In fact, if we represent e as an empty list, we can see that our “multiplication” is nothing but list concatenation.
这种不断生成所有可能的元素组合并执行最少数量的识别(刚好足以遵守法律)的构造称为自由构造。我们刚刚所做的是从生成器集合构造一个自由幺半群{a, b}。
This kind of construction, in which you keep generating all possible combinations of elements, and perform the minimum number of identifications — just enough to uphold the laws — is called a free construction. What we have just done is to construct a free monoid from the set of generators {a, b}.
Haskell 中的二元素集合相当于类型Bool,并且该集合生成的自由幺半群相当于类型[Bool]( 的列表Bool)。(我故意忽略无限列表的问题。)
A two-element set in Haskell is equivalent to the type Bool, and the free monoid generated by this set is equivalent to the type [Bool] (list of Bool). (I am deliberately ignoring problems with infinite lists.)
Haskell 中的幺半群由类型类定义:
A monoid in Haskell is defined by the type class:
class Monoid m where
mempty :: m
mappend :: m -> m -> mclass Monoid m where
mempty :: m
mappend :: m -> m -> m
这只是说每个Monoid必须有一个中性元素,称为mempty,和一个二元函数(乘法)称为mappend。单位定律和结合律无法用 Haskell 表达,并且必须在每次实例化幺半群时由程序员进行验证。
This just says that every Monoid must have a neutral element, which is called mempty, and a binary function (multiplication) called mappend. The unit and associativity laws cannot be expressed in Haskell and must be verified by the programmer every time a monoid is instantiated.
以下实例定义描述了任何类型的列表形成幺半群的事实:
The fact that a list of any type forms a monoid is described by this instance definition:
instance Monoid [a] where
mempty = []
mappend = (++)instance Monoid [a] where
mempty = []
mappend = (++)
它指出空列表[]是单位元素,列表串联(++)是二元运算。
It states that an empty list [] is the unit element, and list concatenation (++) is the binary operation.
正如我们所看到的,类型列表a对应于一个自由幺半群,其中集合a充当生成器。带乘法的自然数集不是自由幺半群,因为我们可以识别很多乘积。比较一下例如:
As we have seen, a list of type a corresponds to a free monoid with the set a serving as generators. The set of natural numbers with multiplication is not a free monoid, because we identify lots of products. Compare for instance:
2 * 3 = 6
[2] ++ [3] = [2, 3] // not the same as [6]2 * 3 = 6
[2] ++ [3] = [2, 3] // not the same as [6]
这很容易,但问题是,我们可以在范畴论中执行这种自由构造吗?不允许我们查看对象的内部?我们将使用我们的主力:通用结构。
That was easy, but the question is, can we perform this free construction in category theory, where we are not allowed to look inside objects? We’ll use our workhorse: the universal construction.
第二个有趣的问题是,通过识别超过法律要求的最小数量的元素,可以从一些自由幺半群中获得任何幺半群吗?我将向您展示这直接来自普遍构造。
The second interesting question is, can any monoid be obtained from some free monoid by identifying more than the minimum number of elements required by the laws? I’ll show you that this follows directly from the universal construction.
如果您还记得我们之前使用通用构造的经验,您可能会注意到,与其说是构造某些东西,不如说是选择最适合给定模式的对象。因此,如果我们想使用通用构造来“构造”一个自由的幺半群,我们必须考虑一大堆幺半群并从中选择一个。我们需要一整类幺半群可供选择。但是幺半群构成一个范畴吗?
If you recall our previous experiences with universal constructions, you might notice that it’s not so much about constructing something as about selecting an object that best fits a given pattern. So if we want to use the universal construction to “construct” a free monoid, we have to consider a whole bunch of monoids from which to pick one. We need a whole category of monoids to chose from. But do monoids form a category?
让我们首先将幺半群视为配备有由单位和乘法定义的附加结构的集合。我们将选择那些保留幺半群结构的函数作为态射。这种结构保持函数称为同态。幺半群同态必须将两个元素的乘积映射到两个元素映射的乘积:
Let’s first look at monoids as sets equipped with additional structure defined by unit and multiplication. We’ll pick as morphisms those functions that preserve the monoidal structure. Such structure-preserving functions are called homomorphisms. A monoid homomorphism must map the product of two elements to the product of the mapping of the two elements:
h (a * b) = h a * h bh (a * b) = h a * h b
并且它必须将单位映射到单位。
例如,考虑从整数列表到整数的同态。如果我们映射[2]到 2 和[3]3,我们必须映射[2, 3]到 6,因为串联
and it must map unit to unit.
For instance, consider a homomorphism from lists of integers to integers. If we map [2] to 2 and [3] to 3, we have to map [2, 3] to 6, because concatenation
[2] ++ [3] = [2, 3][2] ++ [3] = [2, 3]
变成乘法
becomes multiplication
2 * 3 = 62 * 3 = 6
现在让我们忘记单个幺半群的内部结构,只将它们视为具有相应态射的对象。你得到了幺半群的Mon范畴。
Now let’s forget about the internal structure of individual monoids, and only look at them as objects with corresponding morphisms. You get a category Mon of monoids.
好吧,也许在我们忘记内部结构之前,让我们注意一个重要的属性。Mon的每个对象都可以简单地映射到一个集合。它只是其元素的集合。该集合称为底层集合。事实上,我们不仅可以将Mon的对象映射到集合,还可以将Mon的态射(同态)映射到函数。同样,这看起来有点微不足道,但很快就会变得有用。这种对象和态射从Mon到Set 的映射实际上是一个函子。由于这个函子“忘记”了幺半群结构——一旦我们进入一个普通集合,我们就不再区分单位元素或关心乘法——它被称为健忘函子。健忘函子经常出现在范畴论中。
Okay, maybe before we forget about internal structure, let us notice an important property. Every object of Mon can be trivially mapped to a set. It’s just the set of its elements. This set is called the underlying set. In fact, not only can we map objects of Mon to sets, but we can also map morphisms of Mon (homomorphisms) to functions. Again, this seems sort of trivial, but it will become useful soon. This mapping of objects and morphisms from Mon to Set is in fact a functor. Since this functor “forgets” the monoidal structure — once we are inside a plain set, we no longer distinguish the unit element or care about multiplication — it’s called a forgetful functor. Forgetful functors come up regularly in category theory.
我们现在对Mon有两种不同的看法。我们可以像对待任何其他具有对象和态射的范畴一样对待它。从这个角度来看,我们看不到幺半群的内部结构。关于Mon中的特定对象,我们所能说的就是它通过态射与自身和其他对象连接。态射的“乘法”表——组合规则——源自另一种观点:幺半群作为集合。通过范畴论,我们并没有完全失去这个观点——我们仍然可以通过我们的健忘函子来访问它。
We now have two different views of Mon. We can treat it just like any other category with objects and morphisms. In that view, we don’t see the internal structure of monoids. All we can say about a particular object in Mon is that it connects to itself and to other objects through morphisms. The “multiplication” table of morphisms — the composition rules — are derived from the other view: monoids-as-sets. By going to category theory we haven’t lost this view completely — we can still access it through our forgetful functor.
为了应用通用构造,我们需要定义一个特殊的属性,它可以让我们搜索幺半群的范畴并选择自由幺半群的最佳候选者。但自由幺半群是由它的生成器定义的。不同选择的生成器会产生不同的自由幺半群( 的列表Bool与 的列表不同Int)。我们的建设必须从一套发电机开始。所以我们回到片场!
To apply the universal construction, we need to define a special property that would let us search through the category of monoids and pick the best candidate for a free monoid. But a free monoid is defined by its generators. Different choices of generators produce different free monoids (a list of Bool is not the same as a list of Int). Our construction must start with a set of generators. So we’re back to sets!
这就是健忘函子发挥作用的地方。我们可以用它来检查我们的幺半群。我们可以在这些斑点的 X 射线图像中识别发生器。它的工作原理如下:
That’s where the forgetful functor comes into play. We can use it to X-ray our monoids. We can identify the generators in the X-ray images of those blobs. Here’s how it works:
我们从一组生成器开始,x。这是Set中的一个集合。
We start with a set of generators, x. That’s a set in Set.
我们要匹配的模式由一个幺半群(Monoid) ( Monm的一个对象)和Set中的一个函数组成:p
The pattern we are going to match consists of a monoid m — an object of Mon — and a function p in Set:
p :: x -> U mp :: x -> U m
从Mon到Set 的U健忘函子在哪里。这是一种奇怪的异质模式——一半在Mon,一半在Set。
where U is our forgetful functor from Mon to Set. This is a weird heterogeneous pattern — half in Mon and half in Set.
这个想法是,该函数p将识别 的 X 射线图像内的发电机组m。函数在识别集合内的点方面可能很糟糕(它们可能会折叠它们),但这并不重要。一切都将通过通用构造来整理,通用构造将挑选出该模式的最佳代表。
The idea is that the function p will identify the set of generators inside the X-ray image of m. It doesn’t matter that functions may be lousy at identifying points inside sets (they may collapse them). It will all be sorted out by the universal construction, which will pick the best representative of this pattern.
我们还必须定义候选人之间的排名。假设我们有另一个候选者:一个幺半群n和一个在其 X 射线图像中识别生成器的函数:
We also have to define the ranking among candidates. Suppose we have another candidate: a monoid n and a function that identifies the generators in its X-ray image:
q :: x -> U nq :: x -> U n
我们会说这比存在幺半群的态射(即保留结构的同态)m要好:n
We’ll say that m is better than n if there is a morphism of monoids (that’s a structure-preserving homomorphism):
h :: m -> nh :: m -> n
其下的图像U(记住,U是一个函子,因此它将态射映射到函数)通过因式分解p:
whose image under U (remember, U is a functor, so it maps morphisms to functions) factorizes through p:
q = U h . pq = U h . p
如果您考虑p选择 中的生成器m;并q在 中选择“相同”生成器n;那么你可以认为h在两个幺半群之间映射这些生成器。请记住h,根据定义,保留了幺半群结构。这意味着一个幺半群中两个生成器的乘积将映射到第二个幺半群中相应两个生成器的乘积,依此类推。
If you think of p as selecting the generators in m; and q as selecting “the same” generators in n; then you can think of h as mapping these generators between the two monoids. Remember that h, by definition, preserves the monoidal structure. It means that a product of two generators in one monoid will be mapped to a product of the corresponding two generators in the second monoid, and so on.
该排名可用于寻找最佳候选者——自由幺半群。定义如下:
This ranking may be used to find the best candidate — the free monoid. Here’s the definition:
当且仅当从任何其他幺半群m(与函数 一起p)存在满足上述分解性质的唯一态射时,我们才会说(与函数 一起)是带有生成器的自由幺半群。xhmnq
We’ll say that m (together with the function p) is the free monoid with the generators x if and only if there is a unique morphism h from m to any other monoid n (together with the function q) that satisfies the above factorization property.
顺便说一句,这回答了我们的第二个问题。该函数U h能够将 的多个元素折叠U m为 的单个元素U n。这种崩溃对应于识别自由幺半群的一些元素。因此,任何带有生成器的幺半群都可以通过识别一些元素x从自由幺半群中获得。x自由幺半群是只进行了最低限度的识别的一种。
Incidentally, this answers our second question. The function U h is the one that has the power to collapse multiple elements of U m to a single element of U n. This collapse corresponds to identifying some elements of the free monoid. Therefore any monoid with generators x can be obtained from the free monoid based on x by identifying some of the elements. The free monoid is the one where only the bare minimum of identifications have been made.
当我们讨论附加词时,我们会回到自由幺半群。
We’ll come back to free monoids when we talk about adjunctions.
您可能会认为(正如我最初所做的那样)幺半群同态保留单位的要求是多余的。毕竟,我们知道对于所有人来说a
h a * h e = h (a * e) = h a
因此,h e它的作用就像一个右单元(并且,通过类推,就像一个左单元)。问题是h a, for alla可能只覆盖目标幺半群的子幺半群。的图像之外可能存在一个“真实”单位h。证明保留乘法的幺半群之间的同构必须自动保留单位。
You might think (as I did, originally) that the requirement that a homomorphism of monoids preserve the unit is redundant. After all, we know that for all a
h a * h e = h (a * e) = h a
So h e acts like a right unit (and, by analogy, as a left unit). The problem is that h a, for all a might only cover a sub-monoid of the target monoid. There may be a “true” unit outside of the image of h. Show that an isomorphism between monoids that preserves multiplication must automatically preserve unit.
[]?假设所有单例列表都映射到它们包含的整数,即[3]映射到 3 等。 的形象是什么[1, 2, 3, 4]?有多少个不同的列表映射到整数 12?两个幺半群之间还有其他同态吗?[]? Assume that all singleton lists are mapped to the integers they contain, that is [3] is mapped to 3, etc. What’s the image of [1, 2, 3, 4]? How many different lists map to the integer 12? Is there any other homomorphism between the two monoids?我要感谢 Gershom Bazerman 检查我的数学和逻辑,以及 André van Meulebrouck,他在这一系列帖子中自愿提供编辑帮助。
I’d like to thank Gershom Bazerman for checking my math and logic, and André van Meulebrouck, who has been volunteering his editing help throughout this series of posts.
是时候我们来谈谈布景了。数学家对集合论又爱又恨。它是数学的汇编语言——至少以前是。范畴论试图在某种程度上远离集合论。例如,众所周知,所有集合的集合并不存在,但所有集合的范畴Set却存在。所以这样很好。另一方面,我们假设范畴中任意两个对象之间的态射形成一个集合。我们甚至称其为 hom-set。公平地说,范畴论的一个分支中态射不形成集合。相反,它们是另一个范畴的对象。那些使用 hom-objects 而不是 hom-sets 的范畴称为丰富范畴。不过,在下文中,我们将坚持使用良好的老式 hom-sets 范畴。
It’s about time we had a little talk about sets. Mathematicians have a love/hate relationship with set theory. It’s the assembly language of mathematics — at least it used to be. Category theory tries to step away from set theory, to some extent. For instance, it’s a known fact that the set of all sets doesn’t exist, but the category of all sets, Set, does. So that’s good. On the other hand, we assume that morphisms between any two objects in a category form a set. We even called it a hom-set. To be fair, there is a branch of category theory where morphisms don’t form sets. Instead they are objects in another category. Those categories that use hom-objects rather than hom-sets, are called enriched categories. In what follows, though, we’ll stick to categories with good old-fashioned hom-sets.
集合是在分类对象之外最接近无特征斑点的东西。集合有元素,但你不能对这些元素说太多。如果你有一个有限集,你可以计算元素的数量。您可以使用基数来计算无限集合的元素。例如,自然数集小于实数集,尽管两者都是无限的。但是,也许令人惊讶的是,一组有理数与一组自然数的大小相同。
A set is the closest thing to a featureless blob you can get outside of categorical objects. A set has elements, but you can’t say much about these elements. If you have a finite set, you can count the elements. You can kind of count the elements of an inifinite set using cardinal numbers. The set of natural numbers, for instance, is smaller than the set of real numbers, even though both are infinite. But, maybe surprisingly, a set of rational numbers is the same size as the set of natural numbers.
除此之外,关于集合的所有信息都可以编码在它们之间的函数中——尤其是称为同构的可逆函数。就所有意图和目的而言,同构集都是相同的。在我激怒基础数学家之前,让我解释一下,相等和同构之间的区别至关重要。事实上,它是最新数学分支同伦类型理论(HoTT)的主要关注点之一。我之所以提到 HoTT,是因为它是一种从计算中汲取灵感的纯数学理论,其主要支持者之一 Vladimir Voevodsky 在研究 Coq 定理证明者时获得了重大顿悟。数学和编程之间的相互作用是双向的。
Other than that, all the information about sets can be encoded in functions between them — especially the invertible ones called isomorphisms. For all intents and purposes isomorphic sets are identical. Before I summon the wrath of foundational mathematicians, let me explain that the distinction between equality and isomorphism is of fundamental importance. In fact it is one of the main concerns of the latest branch of mathematics, the Homotopy Type Theory (HoTT). I’m mentioning HoTT because it’s a pure mathematical theory that takes inspiration from computation, and one of its main proponents, Vladimir Voevodsky, had a major epiphany while studying the Coq theorem prover. The interaction between mathematics and programming goes both ways.
关于集合的重要教训是,可以比较不同元素的集合。例如,我们可以说一组给定的自然变换与某个态射组同构,因为集合只是一个集合。在这种情况下,同构仅意味着对于一个集合的每个自然变换,都存在来自另一集合的唯一态射,反之亦然。它们可以相互配对。如果苹果和橙子是来自不同范畴的对象,则无法将它们进行比较,但您可以将苹果组与橙子组进行比较。通常,将分类问题转化为集合论问题可以为我们提供必要的洞察力,甚至可以让我们证明有价值的定理。
The important lesson about sets is that it’s okay to compare sets of unlike elements. For instance, we can say that a given set of natural transformations is isomorphic to some set of morphisms, because a set is just a set. Isomorphism in this case just means that for every natural transformation from one set there is a unique morphism from the other set and vice versa. They can be paired against each other. You can’t compare apples with oranges, if they are objects from different categories, but you can compare sets of apples against sets of oranges. Often transforming a categorical problem into a set-theoretical problem gives us the necessary insight or even lets us prove valuable theorems.
每个范畴都配备了到Set 的规范映射系列。这些映射实际上是函子,因此它们保留了范畴的结构。让我们构建一个这样的映射。
Every category comes equipped with a canonical family of mappings to Set. Those mappings are in fact functors, so they preserve the structure of the category. Let’s build one such mapping.
让我们修复Ca中的一个对象并在C中选择另一个对象。hom-set是一个集合,是Set中的一个对象。当我们变化时,保持固定,Set也会变化。因此,我们有一个从到Set 的映射。xC(a, x)xaC(a, x)x
Let’s fix one object a in C and pick another object x also in C. The hom-set C(a, x) is a set, an object in Set. When we vary x, keeping a fixed, C(a, x) will also vary in Set. Thus we have a mapping from x to Set.
如果我们想强调这样一个事实:我们将 hom-set 视为第二个参数中的映射,我们使用以下符号:
If we want to stress the fact that we are considering the hom-set as a mapping in its second argument, we use the notation:
C(a, -)C(a, -)
破折号用作参数的占位符。
with the dash serving as the placeholder for the argument.
这种对象的映射很容易扩展到态射的映射。让我们f在C中计算两个任意对象x和之间的态射y。在我们刚刚定义的映射下x,对象被映射到集合 ,C(a, x)并且对象y被映射到。C(a, y)如果此映射是函子,f则必须映射到两个集合之间的函数:
This mapping of objects is easily extended to the mapping of morphisms. Let’s take a morphism f in C between two arbitrary objects x and y. The object x is mapped to the set C(a, x), and the object y is mapped to C(a, y), under the mapping we have just defined. If this mapping is to be a functor, f must be mapped to a function between the two sets:
C(a, x) -> C(a, y)C(a, x) -> C(a, y)
让我们逐点定义这个函数,即分别为每个参数定义。对于参数,我们应该选择一个任意元素C(a, x)——我们称之为h。如果态射端到端匹配,则它们是可组合的。恰巧 的目标与h的源相匹配f,因此它们的组成:
Let’s define this function point-wise, that is for each argument separately. For the argument we should pick an arbitrary element of C(a, x) — let’s call it h. Morphisms are composable, if they match end to end. It so happens that the target of h matches the source of f, so their composition:
f ∘ h :: a -> yf ∘ h :: a -> y
是从a到 的态射y。因此它是 的成员C(a, y)。
is a morphism going from a to y. It is therefore a member of C(a, y).
我们刚刚找到了从C(a, x)到 的函数C(a, y),它可以作为 的图像f。如果不存在混淆的危险,我们将把这个提升的函数写为:
We have just found our function from C(a, x) to C(a, y), which can serve as the image of f. If there is no danger of confusion, we’ll write this lifted function as:
C(a, f)C(a, f)
及其对态射的作用h为:
and its action on a morphism h as:
C(a, f) h = f ∘ hC(a, f) h = f ∘ h
由于此构造适用于任何范畴,因此它也必须适用于 Haskell 类型的范畴。在 Haskell 中, hom 函子更广为人知的名称是Reader函子:
Since this construction works in any category, it must also work in the category of Haskell types. In Haskell, the hom-functor is better known as the Reader functor:
type Reader a x = a -> xtype Reader a x = a -> x
instance Functor (Reader a) where
fmap f h = f . hinstance Functor (Reader a) where
fmap f h = f . h
现在让我们考虑一下,如果我们不修复 hom-set 的源,而是修复目标,会发生什么。换句话说,我们问的问题是映射是否
Now let’s consider what happens if, instead of fixing the source of the hom-set, we fix the target. In other words, we’re asking the question if the mapping
C(-, a)C(-, a)
也是一个函子。确实如此,但它不是协变的,而是逆变的。f这是因为相同类型的态射端到端匹配会导致;的后合成。而不是预合成,如 的情况C(a, -)。
is also a functor. It is, but instead of being covariant, it’s contravariant. That’s because the same kind of matching of morphisms end to end results in postcomposition by f; rather than precomposition, as was the case with C(a, -).
我们已经在 Haskell 中看到了这个逆变函子。我们称之为Op:
We have already seen this contravariant functor in Haskell. We called it Op:
type Op a x = x -> atype Op a x = x -> a
instance Contravariant (Op a) where
contramap f h = h . finstance Contravariant (Op a) where
contramap f h = h . f
最后,如果我们让两个对象发生变化,我们会得到一个 profunctor C(-, =),它在第一个参数中是逆变的,在第二个参数中是协变的(为了强调两个参数可以独立变化的事实,我们使用双破折号作为第二个占位符)。我们之前在讨论函子性时已经见过这个函子:
Finally, if we let both objects vary, we get a profunctor C(-, =), which is contravariant in the first argument and covariant in the second (to underline the fact that the two arguments may vary independently, we use a double dash as the second placeholder). We have seen this profunctor before, when we talked about functoriality:
instance Profunctor (->) where
dimap ab cd bc = cd . bc . ab
lmap = flip (.)
rmap = (.)instance Profunctor (->) where
dimap ab cd bc = cd . bc . ab
lmap = flip (.)
rmap = (.)
重要的教训是,这个观察结果适用于任何范畴:对象到 hom-set 的映射是函数式的。由于逆变相当于来自相反范畴的映射,因此我们可以将这一事实简洁地表述为:
The important lesson is that this observation holds in any category: the mapping of objects to hom-sets is functorial. Since contravariance is equivalent to a mapping from the opposite category, we can state this fact succintly as:
C(-, =) :: Cop × C -> SetC(-, =) :: Cop × C -> Set
我们已经看到,对于Ca中对象的每一个选择,我们都会得到一个从C到Set 的函子。这种到Set 的结构保留映射通常称为表示。我们将C的对象和态射表示为Set中的集合和函数。
We’ve seen that, for every choice of an object a in C, we get a functor from C to Set. This kind of structure-preserving mapping to Set is often called a representation. We are representing objects and morphisms of C as sets and functions in Set.
函子C(a, -)本身有时被称为可表示的。更一般地,任何与Fhom 函子自然同构的函子(对于 的某些选择a)被称为可表示。这样的函子必须是Set值的,因为C(a, -)是。
The functor C(a, -) itself is sometimes called representable. More generally, any functor F that is naturally isomorphic to the hom-functor, for some choice of a, is called representable. Such functor must necessarily be Set-valued, since C(a, -) is.
我之前说过,我们经常认为同构集合是相同的。更一般地,我们认为范畴中的同构对象是相同的。这是因为对象除了通过态射与其他对象(及其自身)的关系之外没有任何结构。
I said before that we often think of isomorphic sets as identical. More generally, we think of isomorphic objects in a category as identical. That’s because objects have no structure other than their relation to other objects (and themselves) through morphisms.
例如,我们之前讨论过幺半群的范畴Mon,它最初是用集合建模的。但我们小心翼翼地只选择那些保留了这些集合的幺半群结构的函数作为态射。因此,如果Mon中的两个对象是同构的,这意味着它们之间存在可逆态射,那么它们就具有完全相同的结构。如果我们查看它们所基于的集合和函数,我们会发现一个幺半群的单位元素被映射到另一个幺半群的单位元素,并且两个元素的乘积被映射到它们映射的乘积。
For instance, we’ve previously talked about the category of monoids, Mon, that was initially modeled with sets. But we were careful to pick as morphisms only those functions that preserved the monoidal structure of those sets. So if two objects in Mon are isomorphic, meaning there is an invertible morphism between them, they have exactly the same structure. If we peeked at the sets and functions that they were based upon, we’d see that the unit element of one monoid was mapped to the unit element of another, and that a product of two elements was mapped to the product of their mappings.
同样的推理可以应用于函子。两个范畴之间的函子形成一个范畴,其中自然变换扮演态射的角色。因此,如果两个函子之间存在可逆自然变换,则它们是同构的,并且可以被认为是相同的。
The same reasoning can be applied to functors. Functors between two categories form a category in which natural transformations play the role of morphisms. So two functors are isomorphic, and can be thought of as identical, if there is an invertible natural transformation between them.
我们从这个角度来分析一下可表示函子的定义。为了可表示,我们要求: C中F有一个对象;一个自然变换 α 从到;另一个自然变换,β,方向相反;并且它们的组成是同一性的自然变换。aC(a, -)F
Let’s analyze the definition of the representable functor from this perspective. For F to be representable we require that: There be an object a in C; one natural transformation α from C(a, -) to F; another natural transformation, β, in the opposite direction; and that their composition be the identity natural transformation.
让我们看看某个对象 的 α 分量x。这是Set中的一个函数:
Let’s look at the component of α at some object x. It’s a function in Set:
αx :: C(a, x) -> F xαx :: C(a, x) -> F x
此变换的自然性条件告诉我们,对于f从x到的任何态射y,下图可交换:
The naturality condition for this transformation tells us that, for any morphism f from x to y, the following diagram commutes:
F f ∘ αx = αy ∘ C(a, f)F f ∘ αx = αy ∘ C(a, f)
在 Haskell 中,我们将用多态函数替换自然变换:
In Haskell, we would replace natural transformations with polymorphic functions:
alpha :: forall x. (a -> x) -> F xalpha :: forall x. (a -> x) -> F x
与可选forall量词。自然条件
with the optional forall quantifier. The naturality condition
fmap f . alpha = alpha . fmap ffmap f . alpha = alpha . fmap f
由于参数化而自动满足(这是我之前提到的免费定理之一),并且理解fmap左侧是由函子定义的F,而右侧是由读者函子定义的。由于fmap对于读者来说只是函数预组合,所以我们可以更明确。作用于 的h一个元素C(a, x),自然性条件简化为:
is automatically satisfied due to parametricity (it’s one of those theorems for free I mentioned earlier), with the understanding that fmap on the left is defined by the functor F, whereas the one on the right is defined by the reader functor. Since fmap for reader is just function precomposition, we can be even more explicit. Acting on h, an element of C(a, x), the naturality condition simplifies to:
fmap f (alpha h) = alpha (f . h)fmap f (alpha h) = alpha (f . h)
另一种变换 ,beta则以相反的方式进行:
The other transformation, beta, goes the opposite way:
beta :: forall x. F x -> (a -> x)beta :: forall x. F x -> (a -> x)
它必须尊重自然条件,并且必须是 α 的倒数:
It must respect naturality conditions, and it must be the inverse of α:
α ∘ β = id = β ∘ αα ∘ β = id = β ∘ α
C(a, -)稍后我们将看到,从到任何Set值函子的自然变换始终存在(米田引理),但它不一定是可逆的。
We will see later that a natural transformation from C(a, -) to any Set-valued functor always exists (Yoneda’s lemma) but it is not necessarily invertible.
让我给你举一个 Haskell 中带有列表函子和Intas 的例子a。这是完成这项工作的自然转换:
Let me give you an example in Haskell with the list functor and Int as a. Here’s a natural transformation that does the job:
alpha :: forall x. (Int -> x) -> [x]
alpha h = map h [12]alpha :: forall x. (Int -> x) -> [x]
alpha h = map h [12]
我任意选择了数字 12 并用它创建了一个单例列表。然后我可以对该列表执行该fmap函数h并获取 . 返回的类型的列表h。(实际上,有多少个整数列表,就有多少个这样的变换。)
I have arbitrarily picked the number 12 and created a singleton list with it. I can then fmap the function h over this list and get a list of the type returned by h. (There are actually as many such transformations as there are list of integers.)
自然性条件相当于map( 的列表版本fmap)的可组合性:
The naturality condition is equivalent to the composability of map (the list version of fmap):
map f (map h [12]) = map (f . h) [12]map f (map h [12]) = map (f . h) [12]
但是如果我们试图找到逆变换,我们就必须从任意类型的列表转到x返回的函数x:
But if we tried to find the inverse transformation, we would have to go from a list of arbitrary type x to a function returning x:
beta :: forall x. [x] -> (Int -> x)beta :: forall x. [x] -> (Int -> x)
您可能会考虑x从列表中检索 an ,例如使用head,但这对于空列表不起作用。请注意,无法选择此处适用的类型a(代替)。Int所以列表函子是不可表示的。
You might think of retrieving an x from the list, e.g., using head, but that won’t work for an empty list. Notice that there is no choice for the type a (in place of Int) that would work here. So the list functor is not representable.
还记得我们谈到 Haskell (endo-) 函子有点像容器吗?同样,我们可以将可表示函子视为存储函数调用的记忆结果的容器(Haskell 中的 hom-sets 的成员只是函数)。表示对象,即a中的类型C(a, -),被认为是键类型,我们可以使用它访问函数的列表值。我们称为 α 的变换称为tabulate,其逆变换 β 称为index。这是一个(稍微简化的)Representable类定义:
Remember when we talked about Haskell (endo-) functors being a little like containers? In the same vein we can think of representable functors as containers for storing memoized results of function calls (the members of hom-sets in Haskell are just functions). The representing object, the type a in C(a, -), is thought of as the key type, with which we can access the tabulated values of a function. The transformation we called α is called tabulate, and its inverse, β, is called index. Here’s a (slightly simplified) Representable class definition:
class Representable f where
type Rep f :: *
tabulate :: (Rep f -> x) -> f x
index :: f x -> Rep f -> xclass Representable f where
type Rep f :: *
tabulate :: (Rep f -> x) -> f x
index :: f x -> Rep f -> x
a请注意,此处调用的表示类型 ourRep f是 定义的一部分Representable。星号仅意味着它Rep f是一种类型(而不是类型构造函数或其他更奇特的类型)。
Notice that the representing type, our a, which is called Rep f here, is part of the definition of Representable. The star just means that Rep f is a type (as opposed to a type constructor, or other more exotic kinds).
不能为空的无限列表或流是可表示的。
Infinite lists, or streams, which cannot be empty, are representable.
data Stream x = Cons x (Stream x)data Stream x = Cons x (Stream x)
Integer您可以将它们视为以 an作为参数的函数的记忆值。(严格来说,我应该使用非负自然数,但我不想使代码复杂化。)
You can think of them as memoized values of a function taking an Integer as an argument. (Strictly speaking, I should be using non-negative natural numbers, but I didn’t want to complicate the code.)
对于tabulate这样的函数,您可以创建无限的值流。当然,这是可能的,因为 Haskell 很懒。这些值根据需要进行评估。您可以使用以下方法访问存储的值index:
To tabulate such a function, you create an infinite stream of values. Of course, this is only possible because Haskell is lazy. The values are evaluated on demand. You access the memoized values using index:
instance Representable Stream where
type Rep Stream = Integer
tabulate f = Cons (f 0) (tabulate (f . (+1)))
index (Cons b bs) n = if n == 0 then b else index bs (n - 1)instance Representable Stream where
type Rep Stream = Integer
tabulate f = Cons (f 0) (tabulate (f . (+1)))
index (Cons b bs) n = if n == 0 then b else index bs (n - 1)
有趣的是,您可以实现单个记忆方案来覆盖具有任意返回类型的整个函数系列。
It’s interesting that you can implement a single memoization scheme to cover a whole family of functions with arbitrary return types.
逆变函子的可表示性也有类似的定义,只是我们保持第二个参数C(-, a)固定。或者,等价地,我们可以考虑从C op到Set 的函子,因为Cop(a, -)与 相同C(-, a)。
Representability for contravariant functors is similarly defined, except that we keep the second argument of C(-, a) fixed. Or, equivalently, we may consider functors from Cop to Set, because Cop(a, -) is the same as C(-, a).
可代表性有一个有趣的转变。请记住,在笛卡尔封闭范畴中,hom 集在内部可以被视为指数对象。hom-setC(a, x)等价于xa,对于可表示函子,F我们可以这样写:
There is an interesting twist to representability. Remember that hom-sets can internally be treated as exponential objects, in cartesian closed categories. The hom-set C(a, x) is equivalent to xa, and for a representable functor F we can write:
-a = F-a = F
让我们取两边的对数,只是为了好玩:
Let’s take the logarithm of both sides, just for kicks:
a = log Fa = log F
当然,这是一个纯粹的形式变换,但是如果您了解对数的一些属性,这是相当有帮助的。特别是,事实证明,基于产品类型的函子可以用 sum 类型表示,而 sum 类型函子通常不可表示(例如:列表函子)。
Of course, this is a purely formal transformation, but if you know some of the properties of logarithms, it is quite helpful. In particular, it turns out that functors that are based on product types can be represented with sum types, and that sum-type functors are not in general representable (example: the list functor).
最后,请注意,可表示函子为我们提供了同一事物的两种不同实现——一种是函数,一种是数据结构。它们具有完全相同的内容——使用相同的键检索相同的值。这就是我所说的“同一性”的感觉。两个自然同构函子就其内容而言是相同的。另一方面,这两种表示通常以不同的方式实现,并且可能具有不同的性能特征。记忆化被用作性能增强,并且可能会大大减少运行时间。能够生成相同底层计算的不同表示在实践中非常有价值。因此,令人惊讶的是,尽管范畴论根本不关心性能,但它提供了充足的机会来探索具有实用价值的替代实现。
Finally, notice that a representable functor gives us two different implementations of the same thing — one a function, one a data structure. They have exactly the same content — the same values are retrieved using the same keys. That’s the sense of “sameness” I was talking about. Two naturally isomorphic functors are identical as far as their contents are involved. On the other hand, the two representations are often implemented differently and may have different performance characteristics. Memoization is used as a performance enhancement and may lead to substantially reduced run times. Being able to generate different representations of the same underlying computation is very valuable in practice. So, surprisingly, even though it’s not concerned with performance at all, category theory provides ample opportunities to explore alternative implementations that have practical value.
Maybe不具有代表性。Maybe is not representable.Reader?Reader functor representable?Stream表示法,记忆一个平方其参数的函数。Stream representation, memoize a function that squares its argument.tabulate和indexforStream确实是互逆的。(提示:使用归纳法。)tabulate and index for Stream are indeed the inverse of each other. (Hint: use induction.)函子:
Pair a = Pair a a
是有代表性的。你能猜出代表它的类型吗?实施tabulate和index。
The functor:
Pair a = Pair a a
is representable. Can you guess the type that represents it? Implement tabulate and index.
我要感谢 Gershom Bazerman 检查我的数学和逻辑,以及 André van Meulebrouck,他在这一系列帖子中自愿提供编辑帮助。
I’d like to thank Gershom Bazerman for checking my math and logic, and André van Meulebrouck, who has been volunteering his editing help throughout this series of posts.
范畴论中的大多数构造都是其他更具体的数学领域的结果的概括。乘积、余积、幺半群、指数等事物早在范畴论出现之前就已为人所知。它们在不同的数学分支中可能有不同的名称。集合论中的笛卡尔积、顺序理论中的满足、逻辑中的合取——它们都是范畴乘积抽象概念的具体例子。
Most constructions in category theory are generalizations of results from other more specific areas of mathematics. Things like products, coproducts, monoids, exponentials, etc., have been known long before category theory. They might have been known under different names in different branches of mathematics. A cartesian product in set theory, a meet in order theory, a conjunction in logic — they are all specific examples of the abstract idea of a categorical product.
米田引理在这方面脱颖而出,作为关于一般范畴的全面陈述,在其他数学分支中很少或没有先例。有人说它最接近的类似物是群论中的凯莱定理(每个群都同构于某个集合的置换群)。
The Yoneda lemma stands out in this respect as a sweeping statement about categories in general with little or no precedent in other branches of mathematics. Some say that its closest analog is Cayley’s theorem in group theory (every group is isomorphic to a permutation group of some set).
米田引理的设置是任意范畴C以及F从C到Set 的函子。我们在上一节中已经看到,一些Set值函子是可表示的,即与 hom 函子同构。米田引理告诉我们,所有集值函子都可以通过自然变换从 hom 函子获得,并且它明确地枚举了所有此类变换。
The setting for the Yoneda lemma is an arbitrary category C together with a functor F from C to Set. We’ve seen in the previous section that some Set-valued functors are representable, that is isomorphic to a hom-functor. The Yoneda lemma tells us that all Set-valued functors can be obtained from hom-functors through natural transformations, and it explicitly enumerates all such transformations.
当我谈到自然变换时,我提到自然性条件可能具有相当大的限制性。当您在一个对象上定义自然变换的组件时,自然性可能足够强大,可以将该组件“传输”到通过态射与其连接的另一个对象。源范畴和目标范畴中的对象之间的箭头越多,传输自然变换组件的约束就越多。Set恰好是一个箭头非常丰富的范畴。
When I talked about natural transformations, I mentioned that the naturality condition can be quite restrictive. When you define a component of a natural transformation at one object, naturality may be strong enough to “transport” this component to another object that is connected to it through a morphism. The more arrows between objects in the source and the target categories there are, the more constraints you have for transporting the components of natural transformations. Set happens to be a very arrow-rich category.
米田引理告诉我们,hom 函子和任何其他函子之间的自然变换F完全是通过在一个点指定其单个分量的值来完全确定的!其余的自然转变只是根据自然条件而定。
The Yoneda lemma tells us that a natural transformation between a hom-functor and any other functor F is completely determined by specifying the value of its single component at just one point! The rest of the natural transformation just follows from naturality conditions.
那么让我们回顾一下米田引理中涉及的两个函子之间的自然性条件。第一个函子是 hom 函子。它将Cx中的任何对象映射到态射集——对于C中的固定对象。我们还看到它将任何态射从到映射到。C(a, x)afxyC(a, f)
So let’s review the naturality condition between the two functors involved in the Yoneda lemma. The first functor is the hom-functor. It maps any object x in C to the set of morphisms C(a, x) — for a a fixed object in C. We’ve also seen that it maps any morphism f from x to y to C(a, f).
第二个函子是任意Set值函子F。
The second functor is an arbitrary Set-valued functor F.
我们将这两个函子之间的自然变换称为α。因为我们在Set中进行操作,所以自然变换的组件(例如αxor αy)只是集合之间的常规函数:
Let’s call the natural transformation between these two functors α. Because we are operating in Set, the components of the natural transformation, like αx or αy, are just regular functions between sets:
αx :: C(a, x) -> F x
αy :: C(a, y) -> F yαx :: C(a, x) -> F x
αy :: C(a, y) -> F y
因为这些只是函数,所以我们可以在特定点查看它们的值。但这个系列的重点是什么C(a, x)?这是关键的观察结果:集合中的每个点C(a, x)也是h从a到 的态射x。
And because these are just functions, we can look at their values at specific points. But what’s a point in the set C(a, x)? Here’s the key observation: Every point in the set C(a, x) is also a morphism h from a to x.
所以自然性平方为α:
So the naturality square for α:
αy ∘ C(a, f) = F f ∘ αxαy ∘ C(a, f) = F f ∘ αx
当作用于 时,逐点变为h:
becomes, point-wise, when acting on h:
αy (C(a, f) h) = (F f) (αx h)αy (C(a, f) h) = (F f) (αx h)
您可能还记得上一节中的 hom 函子C(a,-)对态射的作用f被定义为预组合:
You might recall from the previous section that the action of the hom-functor C(a,-) on a morphism f was defined as precomposition:
C(a, f) h = f ∘ hC(a, f) h = f ∘ h
这导致:
which leads to:
αy (f ∘ h) = (F f) (αx h)αy (f ∘ h) = (F f) (αx h)
x这个条件有多强可以通过将其专门化为equal to的情况来看出a。
Just how strong this condition is can be seen by specializing it to the case of x equal to a.
在这种情况下,就变成了从到 的h态射。我们知道至少存在一种这样的态射,。让我们将其插入:aah = ida
In that case h becomes a morphism from a to a. We know that there is at least one such morphism, h = ida. Let’s plug it in:
αy f = (F f) (αa ida)αy f = (F f) (αa ida)
注意刚刚发生的事情:左侧是αy对 的任意元素f的操作C(a, y)。它完全由αaat的单个值决定ida。我们可以选择任何这样的值,它将产生自然的转换。由于 的值αa在集合中F a,因此 中的任何点F a都会定义一些α。
Notice what has just happened: The left hand side is the action of αy on an arbitrary element f of C(a, y). And it is totally determined by the single value of αa at ida. We can pick any such value and it will generate a natural transformation. Since the values of αa are in the set F a, any point in F a will define some α.
相反,给定α从C(a, -)到 的任何自然变换F,您可以在 处对其进行评估ida以获得 中的点F a。
Conversely, given any natural transformation α from C(a, -) to F, you can evaluate it at ida to get a point in F a.
我们刚刚证明了米田引理:
We have just proven the Yoneda lemma:
C(a, -)从到 的自然变换F与 的元素之间存在一一对应关系F a。
There is a one-to-one correspondence between natural transformations from C(a, -) to F and elements of F a.
换句话说,
in other words,
Nat(C(a, -), F) ≅ F aNat(C(a, -), F) ≅ F a
或者,如果我们使用C和Set[C, Set]之间的函子范畴表示法,则自然变换的集合只是该范畴中的一个 hom 集,我们可以编写:
Or, if we use the notation [C, Set] for the functor category between C and Set, the set of natural transformation is just a hom-set in that category, and we can write:
[C, Set](C(a, -), F) ≅ F a[C, Set](C(a, -), F) ≅ F a
稍后我将解释这种对应实际上是如何自然同构的。
I’ll explain later how this correspondence is in fact a natural isomorphism.
现在让我们尝试对这个结果有一些直观的了解。最令人惊奇的是,整个自然转变仅从一个成核位点结晶:我们在 处赋予它的值ida。它从该点开始遵循自然条件传播。它淹没了Set中C的图像。那么我们首先考虑C下的图像是什么。C(a, -)
Now let’s try to get some intuition about this result. The most amazing thing is that the whole natural transformation crystallizes from just one nucleation site: the value we assign to it at ida. It spreads from that point following the naturality condition. It floods the image of C in Set. So let’s first consider what the image of C is under C(a, -).
让我们从它本身的形象开始a。在 hom 函子下C(a, -),a被映射到集合C(a, a)。F另一方面,在函子下,它被映射到集合F a。自然变换的组成部分αa是从C(a, a)到 的某个函数F a。让我们只关注集合中的一个点C(a, a),即对应于态射的点ida。为了强调它只是集合中的一个点,我们将其称为p。该组件αa应该映射到中的p某个点。我将向您展示任何选择都会导致独特的自然转变。qF aq
Let’s start with the image of a itself. Under the hom-functor C(a, -), a is mapped to the set C(a, a). Under the functor F, on the other hand, it is mapped to the set F a. The component of the natural transformation αa is some function from C(a, a) to F a. Let’s focus on just one point in the set C(a, a), the point corresponding to the morphism ida. To emphasize the fact that it’s just a point in a set, let’s call it p. The component αa should map p to some point q in F a. I’ll show you that any choice of q leads to a unique natural transformation.
第一个主张是,一个点的选择q唯一地决定了函数的其余部分αa。p'事实上,让我们选择中的任何其他点C(a, a),对应于g从a到的某些态射a。这就是米田引理的神奇之处:g可以被视为p'集合中的一个点C(a, a)。同时,它在组之间选择两个功能。事实上,在 hom 函子下,态射g被映射到一个函数C(a, g);并在F其下方映射到F g.
The first claim is that the choice of one point q uniquely determines the rest of the function αa. Indeed, let’s pick any other point, p' in C(a, a), corresponding to some morphism g from a to a. And here’s where the magic of the Yoneda lemma happens: g can be viewed as a point p' in the set C(a, a). At the same time, it selects two functions between sets. Indeed, under the hom-functor, the morphism g is mapped to a function C(a, g); and under F it’s mapped to F g.
C(a, g)现在让我们考虑一下对我们原来的 的操作p,正如您所记得的,它对应于ida。它被定义为预合成 ,g∘ida它等于g,对应于我们的点p'。因此态射g被映射到一个函数,当作用于 时p产生p',即g。我们已经回到原点了!
Now let’s consider the action of C(a, g) on our original p which, as you remember, corresponds to ida. It is defined as precomposition, g∘ida, which is equal to g, which corresponds to our point p'. So the morphism g is mapped to a function that, when acting on p produces p', which is g. We have come full circle!
F g现在考虑on的动作q。这是一些q',其中的一个点F a。要完成自然性正方形,p'必须映射到q'下αa。我们选择了一个任意的p'(任意的g)并在 下导出了它的映射αa。至此,函数αa就完全确定了。
Now consider the action of F g on q. It is some q', a point in F a. To complete the naturality square, p' must be mapped to q' under αa. We picked an arbitrary p' (an arbitrary g) and derived its mapping under αa. The function αa is thus completely determined.
第二个声明是对于C中连接到 的αx任何对象来说 是唯一确定的。推理过程是类似的,只是现在我们多了两个集合和,并且从到 的态射在 hom 函子下映射到:xaC(a, x)F xgax
The second claim is that αx is uniquely determined for any object x in C that is connected to a. The reasoning is analogous, except that now we have two more sets, C(a, x) and F x, and the morphism g from a to x is mapped, under the hom-functor, to:
C(a, g) :: C(a, a) -> C(a, x)C(a, g) :: C(a, a) -> C(a, x)
并F根据:
and under F to:
F g :: F a -> F xF g :: F a -> F x
同样,C(a, g)对 our 的作用p由预合成给出:g ∘ ida,它对应于p'中的一个点C(a, x)。αx自然性决定了行动的价值p'是:
Again, C(a, g) acting on our p is given by the precomposition: g ∘ ida, which corresponds to a point p' in C(a, x). Naturality determines the value of αx acting on p' to be:
q' = (F g) qq' = (F g) q
由于p'是任意的,因此整个函数αx是确定的。
Since p' was arbitrary, the whole function αx is thus determined.
如果C中存在与 没有连接的对象怎么办a?它们都映射C(a, -)到一个集合——空集合。回想一下,空集是集合范畴中的初始对象。这意味着该集合与任何其他集合都有独特的功能。我们称这个函数为absurd。因此,在这里,我们再次无法选择自然变换的分量:它只能是absurd。
What if there are objects in C that have no connection to a? They are all mapped under C(a, -) to a single set — the empty set. Recall that the empty set is the initial object in the category of sets. It means that there is a unique function from this set to any other set. We called this function absurd. So here, again, we have no choice for the component of the natural transformation: it can only be absurd.
理解米田引理的一种方法是认识到集值函子之间的自然变换只是函数族,并且函数通常是有损的。函数可能会折叠信息,并且可能仅覆盖其密码域的部分内容。唯一没有损耗的函数是可逆的函数——同构。由此可见,最好的结构保留集值函子是可表示的函子。它们要么是 hom 函子,要么是与 hom 函子自然同构的函子。任何其他函子F都是通过有损变换从 hom 函子获得的。这样的变换不仅可能会丢失信息,而且还可能只覆盖SetF中函子图像的一小部分。
One way of understanding the Yoneda lemma is to realize that natural transformations between Set-valued functors are just families of functions, and functions are in general lossy. A function may collapse information and it may cover only parts of its codomain. The only functions that are not lossy are the ones that are invertible — the isomorphisms. It follows then that the best structure-preserving Set-valued functors are the representable ones. They are either the hom-functors or the functors that are naturally isomorphic to hom-functors. Any other functor F is obtained from a hom-functor through a lossy transformation. Such a transformation may not only lose information, but it may also cover only a small part of the image of the functor F in Set.
我们已经在 Haskell 中遇到过以 reader 函子为幌子的 hom-函子:
We have already encountered the hom-functor in Haskell under the guise of the reader functor:
type Reader a x = a -> xtype Reader a x = a -> x
读者通过预组合映射态射(这里是函数):
The reader maps morphisms (here, functions) by precomposition:
instance Functor (Reader a) where
fmap f h = f . hinstance Functor (Reader a) where
fmap f h = f . h
米田引理告诉我们,读者函子可以自然地映射到任何其他函子。
The Yoneda lemma tells us that the reader functor can be naturally mapped to any other functor.
自然变换是多态函数。因此,给定一个函子F,我们有一个从 reader 函子到它的映射:
A natural transformation is a polymorphic function. So given a functor F, we have a mapping to it from the reader functor:
alpha :: forall x . (a -> x) -> F xalpha :: forall x . (a -> x) -> F x
像往常一样,forall是可选的,但我喜欢明确地编写它以强调自然变换的参数多态性。
As usual, forall is optional, but I like to write it explicitly to emphasize parametric polymorphism of natural transformations.
米田引理告诉我们,这些自然变换与 的元素一一对应F a:
The Yoneda lemma tells us that these natural transformations are in one-to-one correspondence with the elements of F a:
forall x . (a -> x) -> F x ≅ F aforall x . (a -> x) -> F x ≅ F a
这个恒等式的右侧就是我们通常认为的数据结构。还记得函子作为广义容器的解释吗?F a是一个容器a。但左侧是一个以函数作为参数的多态函数。米田引理告诉我们,这两种表示是等价的——它们包含相同的信息。
The right hand side of this identity is what we would normally consider a data structure. Remember the interpretation of functors as generalized containers? F a is a container of a. But the left hand side is a polymorphic function that takes a function as an argument. The Yoneda lemma tells us that the two representations are equivalent — they contain the same information.
另一种说法是:给我一个以下类型的多态函数:
Another way of saying this is: Give me a polymorphic function of the type:
alpha :: forall x . (a -> x) -> F xalpha :: forall x . (a -> x) -> F x
我会生产一个容器a。这个技巧就是我们在米田引理证明中使用的技巧:我们调用这个函数来id获取 的元素F a:
and I’ll produce a container of a. The trick is the one we used in the proof of the Yoneda lemma: we call this function with id to get an element of F a:
alpha id :: F aalpha id :: F a
反之亦然:给定一个类型的值F a:
The converse is also true: Given a value of the type F a:
fa :: F afa :: F a
我们可以定义一个多态函数:
one can define a polymorphic function:
alpha h = fmap h faalpha h = fmap h fa
类型正确。您可以轻松地在两种表示形式之间来回切换。
of the correct type. You can easily go back and forth between the two representations.
拥有多种表示的优点是一种表示可能比另一种更容易组合,或者在某些应用程序中一种表示可能比另一种更有效。
The advantage of having multiple representations is that one might be easier to compose than the other, or that one might be more efficient in some applications than the other.
这一原则最简单的说明是编译器构造中经常使用的代码转换:连续传递样式或 CPS。这是米田引理对恒等函子的最简单应用。替换F为恒等式会产生:
The simplest illustration of this principle is the code transformation that is often used in compiler construction: the continuation passing style or CPS. It’s the simplest application of the Yoneda lemma to the identity functor. Replacing F with identity produces:
forall r . (a -> r) -> r ≅ aforall r . (a -> r) -> r ≅ a
这个公式的解释是任何类型都a可以被一个带有“处理程序”的函数替换a。处理程序是一个接受a并执行其余计算(延续)的函数。(该类型r通常封装某种状态代码。)
The interpretation of this formula is that any type a can be replaced by a function that takes a “handler” for a. A handler is a function accepting a and performing the rest of the computation — the continuation. (The type r usually encapsulates some kind of status code.)
这种编程风格在 UI、异步系统和并发编程中非常常见。CPS 的缺点是它涉及控制反转。代码在生产者和消费者(处理程序)之间划分,并且不容易组合。任何完成过大量重要 Web 编程的人都熟悉来自交互状态处理程序的意大利面条代码的噩梦。正如我们稍后将看到的,明智地使用函子和单子可以恢复 CPS 的一些组合属性。
This style of programming is very common in UIs, in asynchronous systems, and in concurrent programming. The drawback of CPS is that it involves inversion of control. The code is split between producers and consumers (handlers), and is not easily composable. Anybody who’s done any amount of nontrivial web programming is familiar with the nightmare of spaghetti code from interacting stateful handlers. As we’ll see later, judicious use of functors and monads can restore some compositional properties of CPS.
像往常一样,我们通过反转箭头的方向来获得额外的构造。米田引理可以应用于相反的范畴C op,为我们提供逆变函子之间的映射。
As usual, we get a bonus construction by inverting the direction of arrows. The Yoneda lemma can be applied to the opposite category Cop to give us a mapping between contravariant functors.
同样,我们可以通过固定 hom 函子的目标对象而不是源来导出 co-Yoneda 引理。我们得到从C到Set 的逆变 hom 函子:C(-, a)。F米田引理的逆变版本在从此函子到任何其他逆变函子的自然变换与集合 的元素之间建立了一一对应关系F a:
Equivalently, we can derive the co-Yoneda lemma by fixing the target object of our hom-functors instead of the source. We get the contravariant hom-functor from C to Set: C(-, a). The contravariant version of the Yoneda lemma establishes one-to-one correspondence between natural transformations from this functor to any other contravariant functor F and the elements of the set F a:
Nat(C(-, a), F) ≅ F aNat(C(-, a), F) ≅ F a
这是联合米田引理的 Haskell 版本:
Here’s the Haskell version of the co-Yoneda lemma:
forall x . (x -> a) -> F x ≅ F aforall x . (x -> a) -> F x ≅ F a
请注意,在某些文献中,逆变版本称为米田引理。
Notice that in some literature it’s the contravariant version that’s called the Yoneda lemma.
证明 Haskell 中形成米田同构的两个函数phi和psi是互逆的。
phi :: (forall x . (a -> x) -> F x) -> F a
phi alpha = alpha id
psi :: F a -> (forall x . (a -> x) -> F x)
psi fa h = fmap h faShow that the two functions phi and psi that form the Yoneda isomorphism in Haskell are inverses of each other.
phi :: (forall x . (a -> x) -> F x) -> F a
phi alpha = alpha id
psi :: F a -> (forall x . (a -> x) -> F x)
psi fa h = fmap h fa[()]除了长度之外不包含其他信息。因此,作为一种数据类型,它可以被视为整数的编码。空列表编码零,单例[()](值,而不是类型)编码一,等等。使用列表函子的米田引理构造此数据类型的另一种表示形式。[()] contains no other information but its length. So, as a data type, it can be considered an encoding of integers. An empty list encodes zero, a singleton [()] (a value, not a type) encodes one, and so on. Construct another representation of this data type using the Yoneda lemma for the list functor.我要感谢 Gershom Bazerman 检查我的数学和逻辑,以及 André van Meulebrouck,他在这一系列帖子中自愿提供编辑帮助。
I’d like to thank Gershom Bazerman for checking my math and logic, and André van Meulebrouck, who has been volunteering his editing help throughout this series of posts.
a我们之前已经看到,当我们修复范畴C中的对象时,映射是从C到SetC(a, -)的(协变)函子。
We’ve seen previously that, when we fix an object a in the category C, the mapping C(a, -) is a (covariant) functor from C to Set.
x -> C(a, x)x -> C(a, x)
(余域是Set,因为 hom 集 C(a, x) 是一个集合。)我们将这种映射称为 hom 函子——我们之前也定义了它对态射的作用。
(The codomain is Set because the hom-set C(a, x) is a set.) We call this mapping a hom-functor — we have previously defined its action on morphisms as well.
现在让我们改变a这个映射。我们得到一个新的映射,将同函子分配给 C(a, -)任何a。
Now let’s vary a in this mapping. We get a new mapping that assigns the hom-functor C(a, -) to any a.
a -> C(a, -)a -> C(a, -)
它是从范畴C到函子的对象的映射,函子是函子范畴中的对象(请参阅自然变换中有关函子范畴的部分)。让我们使用从C到Set 的[C, Set]函子范畴表示法。您可能还记得 hom 函子是典型的可表示函子。
It’s a mapping of objects from category C to functors, which are objects in the functor category (see the section about functor categories in Natural Transformations). Let’s use the notation [C, Set] for the functor category from C to Set. You may also recall that hom-functors are the prototypical representable functors.
每当我们有两个范畴之间的对象映射时,我们很自然地会问这样的映射是否也是一个函子。换句话说,我们是否可以将一个态射从一个范畴提升到另一个范畴的态射。C中的态射只是 的一个元素C(a, b),但函子范畴中的态射[C, Set]是自然变换。所以我们正在寻找态射到自然变换的映射。
Every time we have a mapping of objects between two categories, it’s natural to ask if such a mapping is also a functor. In other words whether we can lift a morphism from one category to a morphism in the other category. A morphism in C is just an element of C(a, b), but a morphism in the functor category [C, Set] is a natural transformation. So we are looking for a mapping of morphisms to natural transformations.
让我们看看能否找到与态射相对应的自然变换f :: a->b。首先,让我们看看a和b映射到什么。它们映射到两个函子:C(a, -)和C(b, -)。我们需要这两个函子之间的自然转换。
Let’s see if we can find a natural transformation corresponding to a morphism f :: a->b. First, lets see what a and b are mapped to. They are mapped to two functors: C(a, -) and C(b, -). We need a natural transformation between those two functors.
技巧如下:我们使用米田引理:
And here’s the trick: we use the Yoneda lemma:
[C, Set](C(a, -), F) ≅ F a[C, Set](C(a, -), F) ≅ F a
并将泛型替换F为 hom-functor C(b, -)。我们得到:
and replace the generic F with the hom-functor C(b, -). We get:
[C, Set](C(a, -), C(b, -)) ≅ C(b, a)[C, Set](C(a, -), C(b, -)) ≅ C(b, a)
这正是我们正在寻找的两个 hom 函子之间的自然变换,但有一点扭曲:我们在自然变换和态射(其中的一个元素)之间有一个映射,它朝着“错误”的方向发展C(b, a)。但没关系;这仅意味着我们正在寻找的函子是逆变的。
This is exactly the natural transformation between the two hom-functors we were looking for, but with a little twist: We have a mapping between a natural transformation and a morphism — an element of C(b, a) — that goes in the “wrong” direction. But that’s okay; it only means that the functor we are looking at is contravariant.
事实上,我们得到的甚至比我们预想的还要多。从C到 的映射[C, Set]不仅是一个逆变函子,而且是一个完全忠实的函子。完整度和忠实度是函子的属性,描述了它们如何映射 hom-set。
Actually, we’ve got even more than we bargained for. The mapping from C to [C, Set] is not only a contravariant functor — it is a fully faithful functor. Fullness and faithfulness are properties of functors that describe how they map hom-sets.
忠实函子在 hom 集上是内射的,这意味着它将不同的态射映射到不同的态射。换句话说,它不会合并它们。
A faithful functor is injective on hom-sets, meaning that it maps distinct morphisms to distinct morphisms. In other words, it doesn’t coalesce them.
完全函子在 hom-set 上是满射的,这意味着它将一个 hom-set 映射到另一个 hom - set,完全覆盖后者。
A full functor is surjective on hom-sets, meaning that it maps one hom-set onto the other hom-set, fully covering the latter.
完全忠实的函子F是hom 集上的双射——两个集合的所有元素的一对一匹配。对于每对对象a和 ,b在源范畴CC(a, b)中,和之间存在双射D(F a, F b),其中D是目标范畴F(在我们的例子中,函子范畴[C, Set])。请注意,这并不意味着这是对象F上的双射。D中可能存在不在 的图像中的对象,并且我们无法对这些对象的 hom 集进行任何描述。F
A fully faithful functor F is a bijection on hom-sets — a one to one matching of all elements of both sets. For every pair of objects a and b in the source category C there is a bijection between C(a, b) and D(F a, F b), where D is the target category of F (in our case, the functor category, [C, Set]). Notice that this doesn’t mean that F is a bijection on objects. There may be objects in D that are not in the image of F, and we can’t say anything about hom-sets for those objects.
我们刚刚描述的(逆变)函子,将C中的对象映射到 中的函子[C, Set]:
The (contravariant) functor we have just described, the functor that maps objects in C to functors in [C, Set]:
a -> C(a, -)a -> C(a, -)
定义Yoneda 嵌入。它将范畴C(严格来说,范畴C op,因为逆变)嵌入函子范畴内。它不仅将C中的对象映射到函子,而且忠实地保留了它们之间的所有连接。[C, Set]
defines the Yoneda embedding. It embeds a category C (strictly speaking, the category Cop, because of contravariance) inside the functor category [C, Set]. It not only maps objects in C to functors, but also faithfully preserves all connections between them.
这是一个非常有用的结果,因为数学家对函子的范畴了解很多,尤其是其余域为Set 的函子。通过将任意范畴C嵌入函子范畴中,我们可以深入了解它。
This is a very useful result because mathematicians know a lot about the category of functors, especially functors whose codomain is Set. We can get a lot of insight about an arbitrary category C by embedding it in the functor category.
当然,米田嵌入有双重版本,有时称为联合米田嵌入。观察到我们可以从修复每个 hom-set 的目标对象(而不是源对象)开始C(-, a)。这会给我们一个逆变 hom 函子。从C到Set 的逆变函子是我们熟悉的 presheaves(例如,参见Limits 和 Colimits)。联合米田嵌入定义了预滑轮范畴中范畴C的嵌入。它对态射的作用由下式给出:
Of course there is a dual version of the Yoneda embedding, sometimes called the co-Yoneda embedding. Observe that we could have started by fixing the target object (rather than the source object) of each hom-set, C(-, a). That would give us a contravariant hom-functor. Contravariant functors from C to Set are our familiar presheaves (see, for instance, Limits and Colimits). The co-Yoneda embedding defines the embedding of a category C in the category of presheaves. Its action on morphisms is given by:
[C, Set](C(-, a), C(-, b)) ≅ C(a, b)[C, Set](C(-, a), C(-, b)) ≅ C(a, b)
同样,数学家对预滑轮的范畴了解很多,因此能够在其中嵌入任意范畴是一个巨大的胜利。
Again, mathematicians know a lot about the category of presheaves, so being able to embed an arbitrary category in it is a big win.
在 Haskell 中,米田嵌入可以表示为一方面读者函子之间的自然变换与另一方面函数(朝相反方向)之间的同构:
In Haskell, the Yoneda embedding can be represented as the isomorphism between natural transformations amongst reader functors on the one hand, and functions (going in the opposite direction) on the other hand:
forall x. (a -> x) -> (b -> x) ≅ b -> aforall x. (a -> x) -> (b -> x) ≅ b -> a
(请记住,读者函子相当于((->) a)。)
(Remember, the reader functor is equivalent to ((->) a).)
这个恒等式的左侧是一个多态函数,给定一个来自ato 的函数x和一个 type 的值b,可以生成一个 type 的值x(我取消柯里化 — 去掉函数周围的括号b -> x)。为所有人做到这一点的唯一方法x是我们的函数知道如何将 a 转换b为a. 它必须秘密地访问某个函数b->a。
The left hand side of this identity is a polymorphic function that, given a function from a to x and a value of type b, can produce a value of type x (I’m uncurrying — dropping the parentheses around — the function b -> x). The only way this can be done for all x is if our function knows how to convert a b to an a. It has to secretly have access to a function b->a.
给定这样一个转换器 ,btoa我们可以定义左侧,将其称为fromY,如下所示:
Given such a converter, btoa, one can define the left hand side, call itfromY, as:
fromY :: (a -> x) -> b -> x
fromY f b = f (btoa b)fromY :: (a -> x) -> b -> x
fromY f b = f (btoa b)
相反,给定一个函数,我们可以通过使用以下身份调用fromY来恢复转换器:fromY
Conversely, given a function fromY we can recover the converter by calling fromY with the identity:
fromY id :: b -> afromY id :: b -> a
fromY这建立了和类型的函数之间的双射btoa。
This establishes the bijection between functions of the type fromY and btoa.
看待这种同构的另一种方法是,它是函数 fromb到的 CPS 编码a。参数a->x是延续(处理程序)。结果是一个函数b,x当使用 type 的值调用该函数时b,将执行与正在编码的函数预先组合的延续。
An alternative way of looking at this isomorphism is that it’s a CPS encoding of a function from b to a. The argument a->x is a continuation (the handler). The result is a function from b to x which, when called with a value of type b, will execute the continuation precomposed with the function being encoded.
Yoneda 嵌入还解释了 Haskell 中数据结构的一些替代表示形式。特别是,它提供了库中镜头的非常有用的表示Control.Lens。
The Yoneda embedding also explains some of the alternative representations of data structures in Haskell. In particular, it provides a very useful representation of lenses from the Control.Lens library.
这个例子是由罗伯特·哈珀提出的。这是米田嵌入到预购定义的范畴中的应用。预序是一个集合,其元素之间具有顺序关系,传统上写为<=(小于或等于)。先序中的“pre”之所以存在,是因为我们只要求关系是传递和自反的,但不一定是反对称的(因此可能有循环)。
This example was suggested by Robert Harper. It’s the application of the Yoneda embedding to a category defined by a preorder. A preorder is a set with an ordering relation between its elements that’s traditionally written as <= (less than or equal). The “pre” in preorder is there because we’re only requiring the relation to be transitive and reflexive but not necessarily antisymmetric (so it’s possible to have cycles).
具有先序关系的集合产生一个范畴。对象是该集合的元素。如果对象无法比较或者不成立,则从对象到任一对象的态a射b都不存在a <= b;或者它存在如果a <= b,并且它指向从a到b。从一个对象到另一个对象的态射绝不会超过一种。因此,此类范畴中的任何 hom-set 要么是空集,要么是单元素集。这样的范畴称为瘦。
A set with the preorder relation gives rise to a category. The objects are the elements of this set. A morphism from object a to b either doesn’t exist, if the objects cannot be compared or if it’s not true that a <= b; or it exists if a <= b, and it points from a to b. There is never more than one morphism from one object to another. Therefore any hom-set in such a category is either an empty set or a one-element set. Such a category is called thin.
很容易让自己相信这种构造确实是一个范畴:箭头是可组合的,因为 ifa <= b和b <= cthen a <= c; 并且组合是关联的。我们还有恒等箭头,因为每个元素都(小于或)等于自身(底层关系的自反性)。
It’s easy to convince yourself that this construction is indeed a category: The arrows are composable because, if a <= b and b <= c then a <= c; and the composition is associative. We also have the identity arrows because every element is (less than or) equal to itself (reflexivity of the underlying relation).
我们现在可以将联合米田嵌入应用于预购范畴。我们特别感兴趣的是它对态射的作用:
We can now apply the co-Yoneda embedding to a preorder category. In particular, we’re interested in its action on morphisms:
[C, Set](C(-, a), C(-, b)) ≅ C(a, b)[C, Set](C(-, a), C(-, b)) ≅ C(a, b)
右侧的 hom-set 是非空的当且仅当a <= b——在这种情况下它是一个单元素集。因此,如果a <= b,则左侧存在单个自然变换。否则就没有自然的转变。
The hom-set on the right hand side is non-empty if and only if a <= b — in which case it’s a one-element set. Consequently, if a <= b, there exists a single natural transformation on the left. Otherwise there is no natural transformation.
那么预序中 hom 函子之间的自然变换是什么呢?C(-, a)它应该是介于 set和之间的一系列函数C(-, b)。在预购中,每个集合可以是空的,也可以是单例的。让我们看看有哪些功能可供我们使用。
So what’s a natural transformation between hom-functors in a preorder? It should be a family of functions between sets C(-, a) and C(-, b). In a preorder, each of these sets can either be empty or a singleton. Let’s see what kind of functions are there at our disposal.
有一个从空集到它自身的函数(作用于空集的恒等),有一个absurd从空集到单例集的函数(它什么也不做,因为它只需要为空集的元素定义,没有),以及从单例到自身的函数(作用于单元素集的恒等式)。唯一被禁止的组合是从单例到空集的映射(当作用于单个元素时,这样的函数的值是什么?)。
There is a function from an empty set to itself (the identity acting on an empty set), a function absurd from an empty set to a singleton set (it does nothing, since it only needs to be defined for elements of an empty set, of which there are none), and a function from a singleton to itself (the identity acting on a one-element set). The only combination that is forbidden is the mapping from a singleton to an empty set (what would the value of such a function be when acting on the single element?).
因此,我们的自然变换永远不会将单个 hom-set 连接到空 hom-set。换句话说, if x <= a(singleton hom-set C(x, a)) thenC(x, b)不能为空。非空C(x, b)意味着x小于或等于b。因此,所讨论的自然变换的存在要求,对于每一个x,如果x <= a那么x <= b。
So our natural transformation will never connect a singleton hom-set to an empty hom-set. In other words, if x <= a (singleton hom-set C(x, a)) then C(x, b) cannot be empty. A non-empty C(x, b) means that x is less or equal to b. So the existence of the natural transformation in question requires that, for every x, if x <= a then x <= b.
for all x, x ≤ a ⇒ x ≤ bfor all x, x ≤ a ⇒ x ≤ b
另一方面,co-Yoneda 告诉我们,这种自然变换的存在相当于C(a, b)非空,或者相当于a <= b。一起,我们得到:
On the other hand, co-Yoneda tells us that the existence of this natural transformation is equivalent to C(a, b) being non-empty, or to a <= b. Together, we get:
a ≤ b if and only if for all x, x ≤ a ⇒ x ≤ ba ≤ b if and only if for all x, x ≤ a ⇒ x ≤ b
我们本来可以直接得出这个结果。直觉是,如果a <= b则下面的所有元素a也必须位于 之下b。相反,当您替换a右侧x时,则得出a <= b。但你必须承认,通过米田嵌入得到这个结果要令人兴奋得多。
We could have arrived at this result directly. The intuition is that, if a <= b then all elements that are below a must also be below b. Conversely, when you substitute a for x on the right hand side, it follows that a <= b. But you must admit that arriving at this result through the Yoneda embedding is much more exciting.
米田引理建立了自然变换集与Set中的对象之间的同构。自然变换是函子范畴中的态射[C, Set]。任何两个函子之间的自然变换集是该范畴中的 hom 集。米田引理是同构:
The Yoneda lemma establishes the isomorphism between the set of natural transformations and an object in Set. Natural transformations are morphisms in the functor category [C, Set]. The set of natural transformation between any two functors is a hom-set in that category. The Yoneda lemma is the isomorphism:
[C, Set](C(a, -), F) ≅ F a[C, Set](C(a, -), F) ≅ F a
F事实证明,这种同构在和中都是自然的a。换句话说,它很自然地出现在 中(F, a),一对取自产品范畴[C, Set] × C。请注意,我们现在将其视为F函子范畴中的对象。
This isomorphism turns out to be natural in both F and a. In other words, it’s natural in (F, a), a pair taken from the product category [C, Set] × C. Notice that we are now treating F as an object in the functor category.
让我们想一下这意味着什么。自然同构是两个函子之间的可逆自然变换。事实上,同构的右侧是一个函子。它是一个从[C, Set] × C到Set 的函子。它对一对的作用是一个集合——对对象 处的(F, a)函子求值的结果。这称为评估函子。Fa
Let’s think for a moment what this means. A natural isomorphism is an invertible natural transformation between two functors. And indeed, the right hand side of our isomorphism is a functor. It’s a functor from [C, Set] × C to Set. Its action on a pair (F, a) is a set — the result of evaluating the functor F at the object a. This is called the evaluation functor.
左侧也是一个函子,它进行(F, a)一组自然变换[C, Set](C(a, -), F)。
The left hand side is also a functor that takes (F, a) to a set of natural transformations [C, Set](C(a, -), F).
为了证明它们确实是函子,我们还应该定义它们对态射的作用。(F, a)但是一对和之间的态射是什么(G, b)?这是一对态射,(Φ, f); 第一个是函子之间的态射——自然变换——第二个是C中的常规态射。
To show that these are really functors, we should also define their action on morphisms. But what’s a morphism between a pair (F, a) and (G, b)? It’s a pair of morphisms, (Φ, f); the first being a morphism between functors — a natural transformation — the second being a regular morphism in C.
求值函子采用该对并将其映射到两个集合和(Φ, f)之间的函数。我们可以很容易地从at的分量(映射到)和由 提升的态射构造这样的函数:F aG bΦaF aG afG
The evaluation functor takes this pair (Φ, f) and maps it to a function between two sets, F a and G b. We can easily construct such a function from the component of Φ at a (which maps F a to G a) and the morphism f lifted by G:
(G f) ∘ Φa(G f) ∘ Φa
请注意,由于 的自然性Φ,这与以下内容相同:
Notice that, because of naturality of Φ, this is the same as:
Φb ∘ (F f)Φb ∘ (F f)
我不会证明整个同构的自然性——在你确定函子是什么之后,证明就相当机械了。这是因为我们的同构是由函子和自然变换建立起来的。它根本不可能出错。
I’m not going to prove the naturality of the whole isomorphism — after you’ve established what the functors are, the proof is pretty mechanical. It follows from the fact that our isomorphism is built up from functors and natural transformations. There is simply no way for it to go wrong.
fromY和 之间建立的双射btoa是同构(两个映射互为逆)。fromY and btoa is an isomorphism (the two mappings are the inverse of each other).[C, D]在函子范畴中嵌入任意函子范畴[[C, D], Set]。弄清楚它如何处理态射(在本例中是自然变换)。[C, D] in the functor category [[C, D], Set]. Figure out how it works on morphisms (which in this case are natural transformations).我要感谢 Gershom Bazerman 检查我的数学和逻辑。
I’d like to thank Gershom Bazerman for checking my math and logic.
如果我还没有让你相信范畴论就是态射,那么我就没有做好我的工作。由于下一个主题是附加词,它是根据 hom 集的同构来定义的,因此回顾我们对 hom 集构建块的直觉是有意义的。此外,您还会发现附加词提供了一种更通用的语言来描述我们之前研究过的许多结构,因此回顾它们也可能有所帮助。
If I haven’t convinced you yet that category theory is all about morphisms then I haven’t done my job properly. Since the next topic is adjunctions, which are defined in terms of isomorphisms of hom-sets, it makes sense to review our intuitions about the building blocks of hom-sets. Also, you’ll see that adjunctions provide a more general language to describe a lot of constructions we’ve studied before, so it might help to review them too.
首先,您确实应该将函子视为态射的映射——类型类的 Haskell 定义中强调的观点Functor,它围绕fmap. 当然,函子也映射对象——态射的端点——否则我们就无法谈论保留组合。对象告诉我们哪些态射对是可组合的。如果要组合它们,一个态射的目标必须等于另一个态射的源。因此,如果我们希望将态射的组合映射到提升态射的组合,那么它们端点的映射就基本上确定了。
To begin with, you should really think of functors as mappings of morphisms — the view that’s emphasized in the Haskell definition of the Functor typeclass, which revolves around fmap. Of course, functors also map objects — the endpoints of morphisms — otherwise we wouldn’t be able to talk about preserving composition. Objects tell us which pairs of morphisms are composable. The target of one morphism must be equal to the source of the other — if they are to be composed. So if we want the composition of morphisms to be mapped to the composition of lifted morphisms, the mapping of their endpoints is pretty much determined.
态射的许多性质都是用通勤图来表达的。如果一个特定的态射可以用多种方式描述为其他态射的组合,那么我们就有了交换图。
A lot of properties of morphisms are expressed in terms of commuting diagrams. If a particular morphism can be described as a composition of other morphisms in more than one way, then we have a commuting diagram.
特别是,通勤图构成了几乎所有通用结构的基础(初始和终止对象除外)。我们已经在乘积、余积、各种其他(共)极限、指数对象、自由幺半群等的定义中看到了这一点。
In particular, commuting diagrams form the basis of almost all universal constructions (with the notable exceptions of the initial and terminal objects). We’ve seen this in the definitions of products, coproducts, various other (co-)limits, exponential objects, free monoids, etc.
该产品是通用结构的一个简单示例。我们选择两个对象a和b,看看是否存在一个对象c,以及一对态射p和q,它具有作为它们的乘积的通用属性。
The product is a simple example of a universal construction. We pick two objects a and b and see if there exists an object c, together with a pair of morphisms p and q, that has the universal property of being their product.
产品是极限的特例。极限是根据锥体定义的。通用锥体是根据通勤图构建的。这些图的交换性可以用函子映射的合适的自然性条件来代替。通过这种方式,交换性被简化为汇编语言对于自然转换的高级语言的作用。
A product is a special case of a limit. A limit is defined in terms of cones. A general cone is built from commuting diagrams. Commutativity of those diagrams may be replaced with a suitable naturality condition for the mapping of functors. This way commutativity is reduced to the role of the assembly language for the higher level language of natural transformations.
一般来说,每当我们需要从态射到交换平方的映射时,自然变换就非常方便。f自然性正方形的两个相对边是某些态射在两个函子F和下的映射G。另一边是自然变换的组成部分(也是态射)。
In general, natural transformations are very convenient whenever we need a mapping from morphisms to commuting squares. Two opposing sides of a naturality square are the mappings of some morphism f under two functors F and G. The other sides are the components of the natural transformation (which are also morphisms).
自然性意味着当您移动到“相邻”组件(我所说的相邻是指通过态射连接)时,您不会违背范畴或函子的结构。是否首先使用自然变换的组件来桥接对象之间的间隙,然后使用函子跳转到其邻居,这并不重要;或者反之亦然。这两个方向是正交的。可以这么说,自然变换使您左右移动,函子使您上下或前后移动。您可以将函子的图像可视化为目标范畴中的一张纸。自然变换将对应于 F 的这样的一张纸映射到对应于 G 的另一张纸。
Naturality means that when you move to the “neighboring” component (by neighboring I mean connected by a morphism), you’re not going against the structure of either the category or the functors. It doesn’t matter whether you first use a component of the natural transformation to bridge the gap between objects, and then jump to its neighbor using the functor; or the other way around. The two directions are orthogonal. A natural transformation moves you left and right, and the functors move you up and down or back and forth — so to speak. You can visualize the image of a functor as a sheet in the target category. A natural transformation maps one such sheet corresponding to F, to another, corresponding to G.
我们已经在 Haskell 中看到了这种正交性的例子。函子的动作修改容器的内容而不改变其形状,而自然转换将未触及的内容重新打包到不同的容器中。这些操作的顺序并不重要。
We’ve seen examples of this orthogonality in Haskell. There the action of a functor modifies the content of a container without changing its shape, while a natural transformation repackages the untouched contents into a different container. The order of these operations doesn’t matter.
我们已经看到极限定义中的锥体被自然变换所取代。自然性确保每个锥体的侧面都能通勤。尽管如此,限制还是根据锥体之间的映射来定义的。这些映射还必须满足交换性条件。(例如,乘积定义中的三角形必须交换。)
We’ve seen the cones in the definition of a limit replaced by natural transformations. Naturality ensures that the sides of every cone commute. Still, a limit is defined in terms of mappings between cones. These mappings must also satisfy commutativity conditions. (For instance, the triangles in the definition of the product must commute.)
这些条件也可能被自然性所取代。您可能还记得通用锥体或极限被定义为(逆变)hom 函子之间的自然变换:
These conditions, too, may be replaced by naturality. You may recall that the universal cone, or the limit, is defined as a natural transformation between the (contravariant) hom-functor:
F :: c -> C(c, Lim D)F :: c -> C(c, Lim D)
以及将C中的对象映射到锥体的(也是逆变的)函子,它们本身就是自然变换:
and the (also contravariant) functor that maps objects in C to cones, which themselves are natural transformations:
G :: c -> Nat(Δc, D)G :: c -> Nat(Δc, D)
这里,Δc是常量函子,是定义CD中的图的函子。函子和都对C中的态射有明确定义的动作。碰巧和之间的这种特殊的自然变换是同构。FGFG
Here, Δc is the constant functor, and D is the functor that defines the diagram in C. Both functors F and G have well defined actions on morphisms in C. It so happens that this particular natural transformation between F and G is an isomorphism.
自然同构——这是一种自然变换,其每个组成部分都是可逆的——是范畴论表达“两个事物是相同的”的方式。这种变换的一个组成部分必须是对象之间的同构——具有逆的态射。如果将函子图像可视化为薄片,那么自然同构就是这些薄片之间的一对一可逆映射。
A natural isomorphism — which is a natural transformation whose every component is reversible — is category theory’s way of saying that “two things are the same.” A component of such a transformation must be an isomorphism between objects — a morphism that has the inverse. If you visualize functor images as sheets, a natural isomorphism is a one-to-one invertible mapping between those sheets.
但什么是态射呢?它们确实比对象具有更多的结构:与对象不同,态射有两端。但是,如果固定源对象和目标对象,则两者之间的态射形成一个无聊的集合(至少对于局部小范畴)。我们可以给这个集合的元素命名,例如f或g,以区分彼此 - 但到底是什么让它们不同呢?
But what are morphisms? They do have more structure than objects: unlike objects, morphisms have two ends. But if you fix the source and the target objects, the morphisms between the two form a boring set (at least for locally small categories). We can give elements of this set names like f or g, to distinguish one from another — but what is it, really, that makes them different?
给定 hom 集中态射之间的本质区别在于它们与其他态射(来自邻接 hom 集)的组合方式。如果存在一个态射h,其组成(前或后) 与f不同g,例如:
The essential difference between morphisms in a given hom-set lies in the way they compose with other morphisms (from abutting hom-sets). If there is a morphism h whose composition (either pre- or post-) with f is different than that with g, for instance:
h ∘ f ≠ h ∘ gh ∘ f ≠ h ∘ g
f那么我们就可以直接“观察”和之间的差异g。但即使差异不能直接观察到,我们也可以使用函子来放大 hom-set。函子F可以将两个态射映射到不同的态射:
then we can directly “observe” the difference between f and g. But even if the difference is not directly observable, we might use functors to zoom in on the hom-set. A functor F may map the two morphisms to distinct morphisms:
F f ≠ F gF f ≠ F g
在更丰富的范畴中,相邻的 hom 集提供更高的分辨率,例如,
in a richer category, where the abutting hom-sets provide more resolution, e.g.,
h' ∘ F f ≠ h' ∘ F gh' ∘ F f ≠ h' ∘ F g
哪里h'不在 的图像中F。
where h' is not in the image of F.
许多分类结构依赖于 hom 集之间的同构。但由于 hom-set 只是集合,它们之间的简单同构并不能告诉你太多信息。对于有限集,同构只是说它们具有相同数量的元素。如果集合是无限的,则它们的基数必须相同。但任何有意义的同构同构都必须考虑组合。并且组合涉及多个本集。我们需要定义跨越整个 hom-set 集合的同构,并且需要施加一些与组合互操作的兼容性条件。自然的同构正好符合这个要求。
A lot of categorical constructions rely on isomorphisms between hom-sets. But since hom-sets are just sets, a plain isomorphism between them doesn’t tell you much. For finite sets, an isomorphism just says that they have the same number of elements. If the sets are infinite, their cardinality must be the same. But any meaningful isomorphism of hom-sets must take into account composition. And composition involves more than one hom-set. We need to define isomorphisms that span whole collections of hom-sets, and we need to impose some compatibility conditions that interoperate with composition. And a natural isomorphism fits the bill exactly.
但是 hom 集的自然同构是什么?自然性是函子之间映射的属性,而不是集合。所以我们真正谈论的是 hom 集值函子之间的自然同构。这些函子不仅仅是定值函子。它们对态射的作用是由适当的 hom 函子引发的。态射由 hom 函子使用预组合或后组合(取决于函子的协方差)进行规范映射。
But what’s a natural isomorphism of hom-sets? Naturality is a property of mappings between functors, not sets. So we are really talking about a natural isomorphism between hom-set-valued functors. These functors are more than just set-valued functors. Their action on morphisms is induced by the appropriate hom-functors. Morphisms are canonically mapped by hom-functors using either pre- or post-composition (depending on the covariance of the functor).
米田嵌入就是这种同构的一个例子。它将C中的 hom-sets 映射到函子范畴中的 hom-sets;这是很自然的。米田嵌入中的一个函子是C中的 hom 函子,另一个将对象映射到 hom 集之间的自然变换集。
The Yoneda embedding is one example of such an isomorphism. It maps hom-sets in C to hom-sets in the functor category; and it’s natural. One functor in the Yoneda embedding is the hom-functor in C and the other maps objects to sets of natural transformations between hom-sets.
极限的定义也是 hom 集之间的自然同构(第二个同构,同样在函子范畴中):
The definition of a limit is also a natural isomorphism between hom-sets (the second one, again, in the functor category):
C(c, Lim D) ≃ Nat(Δc, D)C(c, Lim D) ≃ Nat(Δc, D)
事实证明,我们构建的指数对象或自由幺半群也可以重写为 hom 集之间的自然同构。
It turns out that our construction of an exponential object, or that of a free monoid, can also be rewritten as a natural isomorphism between hom-sets.
这并非巧合——接下来我们将看到这些只是附加词的不同示例,它们被定义为 hom 集的自然同构。
This is no coincidence — we’ll see next that these are just different examples of adjunctions, which are defined as natural isomorphisms of hom-sets.
还有一项观察可以帮助我们理解附加语。一般来说,Hom 组不是对称的。hom-setC(a, b)通常与 hom-set 有很大不同C(b, a)。这种不对称性的最终体现是被视为范畴的偏序。在偏序中,当且仅当小于或等于时,存在从a到 的态射。如果和不同,那么就不可能有相反的态射,即从到。因此,如果 hom-set非空(在本例中意味着它是一个单例集),则必须为空,除非。这一范畴中的箭头在一个方向上有明确的流动。bababbaC(a, b)C(b, a)a = b
There is one more observation that will help us understand adjunctions. Hom-sets are, in general, not symmetric. A hom-set C(a, b) is often very different from the hom-set C(b, a). The ultimate demonstration of this asymmetry is a partial order viewed as a category. In a partial order, a morphism from a to b exists if and only if a is less than or equal to b. If a and b are different, then there can be no morphism going the other way, from b to a. So if the hom-set C(a, b) is non-empty, which in this case means it’s a singleton set, then C(b, a) must be empty, unless a = b. The arrows in this category have a definite flow in one direction.
基于不一定是反对称的关系的预序也“大部分”是有方向的,除了偶尔的循环。将任意范畴视为预编码器的概括是很方便的。
A preorder, which is based on a relation that’s not necessarily antisymmetric, is also “mostly” directional, except for occasional cycles. It’s convenient to think of an arbitrary category as a generalization of a preoder.
预购是一个薄范畴——所有 hom-set 要么是单例,要么是空的。我们可以将一般范畴想象为“厚”预购。
A preorder is a thin category — all hom-sets are either singletons or empty. We can visualize a general category as a “thick” preorder.
F或G将两个对象a和b( 的末尾f :: a -> b)映射到同一个对象(例如F a = F b或),会发生什么G a = G b?(请注意,您可以通过这种方式得到一个圆锥体或一个共圆锥体。)然后考虑 或 的F a = G a情况F b = G b。最后,如果你从一个自身循环的态射开始会怎样f :: a -> a?F or G map both objects a and b (the ends of f :: a -> b) to the same object, e.g., F a = F b or G a = G b? (Notice that you get a cone or a co-cone this way.) Then consider cases where either F a = G a or F b = G b. Finally, what if you start with a morphism that loops on itself — f :: a -> a?我要感谢 Gershom Bazerman 检查我的数学和逻辑,以及 André van Meulebrouck,他在这一系列帖子中自愿提供编辑帮助。
I’d like to thank Gershom Bazerman for checking my math and logic, and André van Meulebrouck, who has been volunteering his editing help throughout this series of posts.
在数学中,我们有多种方式来表示一件事与另一件事相似。最严格的是平等。如果没有办法区分一者与另一者,则两件事是平等的。在任何可以想象的情况下,一种都可以替代另一种。例如,您是否注意到我们每次讨论通勤图时都使用态射相等?这是因为态射形成一个集合(hom-set),并且可以比较集合元素是否相等。
In mathematics we have various ways of saying that one thing is like another. The strictest is equality. Two things are equal if there is no way to distinguish one from another. One can be substituted for the other in every imaginable context. For instance, did you notice that we used equality of morphisms every time we talked about commuting diagrams? That’s because morphisms form a set (hom-set) and set elements can be compared for equality.
但平等往往太过强烈。有很多例子表明事物在所有意图和目的上都是相同的,但实际上并不相等。例如,pair 类型(Bool, Char)并不严格等于(Char, Bool),但我们知道它们包含相同的信息。这个概念最好通过两种类型之间的同构来体现——可逆的态射。由于它是态射,因此它保留了结构;“iso”意味着它是往返旅程的一部分,无论您从哪一边开始,都会到达同一个地点。在成对的情况下,这种同构称为swap:
But equality is often too strong. There are many examples of things being the same for all intents and purposes, without actually being equal. For instance, the pair type (Bool, Char) is not strictly equal to (Char, Bool), but we understand that they contain the same information. This concept is best captured by an isomorphism between two types — a morphism that’s invertible. Since it’s a morphism, it preserves the structure; and being “iso” means that it’s part of a round trip that lands you in the same spot, no matter on which side you start. In the case of pairs, this isomorphism is called swap:
swap :: (a,b) -> (b,a)
swap (a,b) = (b,a)swap :: (a,b) -> (b,a)
swap (a,b) = (b,a)
swap恰好是它自己的逆。
swap happens to be its own inverse.
当我们谈论范畴同构时,我们用范畴之间的映射(又称函子)来表达这一点。如果存在从C到D的可逆函子(“右”) ,我们希望能够说两个范畴C和D是同构的。换句话说,从D回到C存在另一个函子(“左”),当与 组成时,它等于恒等函子。有两种可能的组合,并且;以及两个可能的恒等函子:一个在C中,另一个在D中。RLRIR ∘ LL ∘ R
When we talk about categories being isomorphic, we express this in terms of mappings between categories, a.k.a. functors. We would like to be able to say that two categories C and D are isomorphic if there exists a functor R (“right”) from C to D, which is invertible. In other words, there exists another functor L (“left”) from D back to C which, when composed with R, is equal to the identity functor I. There are two possible compositions, R ∘ L and L ∘ R; and two possible identity functors: one in C and another in D.
但这是棘手的部分:两个函子相等意味着什么?这种平等是什么意思:
But here’s the tricky part: What does it mean for two functors to be equal? What do we mean by this equality:
R ∘ L = IDR ∘ L = ID
或者这个:
or this one:
L ∘ R = ICL ∘ R = IC
根据对象的相等性来定义函子相等性是合理的。两个函子作用于相同的对象时,应该产生相同的对象。但一般来说,我们没有任意范畴中的对象相等的概念。它只是不是定义的一部分。(深入探讨“平等到底是什么”这个兔子洞,我们最终会得出同伦类型理论。)
It would be reasonable to define functor equality in terms of equality of objects. Two functors, when acting on equal objects, should produce equal objects. But we don’t, in general, have the notion of object equality in an arbitrary category. It’s just not part of the definition. (Going deeper into this rabbit hole of “what equality really is,” we would end up in Homotopy Type Theory.)
您可能会认为函子是范畴范畴中的态射,因此它们应该是相等可比较的。事实上,只要我们谈论的是小范畴,其中对象构成一个集合,我们确实可以使用集合元素的相等性来对对象进行相等比较。
You might argue that functors are morphisms in the category of categories, so they should be equality-comparable. And indeed, as long as we are talking about small categories, where objects form a set, we can indeed use the equality of elements of a set to equality-compare objects.
但是,请记住,Cat实际上是一个二分类。2 范畴中的 Hom 集具有额外的结构——1 态射之间有 2 态射作用。在Cat中,1-态射是函子,2-态射是自然变换。因此,在谈论函子时,将自然同构视为平等的替代品是更自然的(无法避免这个双关语!)。
But, remember, Cat is really a 2-category. Hom-sets in a 2-category have additional structure — there are 2-morphisms acting between 1-morphisms. In Cat, 1-morphisms are functors, and 2-morphisms are natural transformations. So it’s more natural (can’t avoid this pun!) to consider natural isomorphisms as substitutes for equality when talking about functors.
因此,考虑更一般的等价概念而不是范畴的同构是有意义的。如果我们能找到两个在它们之间来回的函子,并且其组合(无论哪种方式)自然地与恒等函子同构,则两个范畴C和D是等价的。换句话说,组合和恒等函子之间以及和恒等函子之间存在双向自然变换。R ∘ LIDL ∘ RIC
So, instead of isomorphism of categories, it makes sense to consider a more general notion of equivalence. Two categories C and D are equivalent if we can find two functors going back and forth between them, whose composition (either way) is naturally isomorphic to the identity functor. In other words, there is a two-way natural transformation between the composition R ∘ L and the identity functor ID, and another between L ∘ R and the identity functor IC.
附加甚至比等价更弱,因为它不要求两个函子的组合与恒等函子同构。相反,它规定存在一种从到 的单向自然变换,以及另一种从到 的自然变换。以下是这两个自然变换的签名:IDR∘LL∘RIC
Adjunction is even weaker than equivalence, because it doesn’t require that the composition of the two functors be isomorphic to the identity functor. Instead it stipulates the existence of a one way natural transformation from ID to R∘L, and another from L∘R to IC. Here are the signatures of these two natural transformations:
η :: ID -> R ∘ L
ε :: L ∘ R -> ICη :: ID -> R ∘ L
ε :: L ∘ R -> IC
η 称为附加词的单位,ε 称为附加词的单位。
η is called the unit, and ε the counit of the adjunction.
请注意这两个定义之间的不对称性。一般来说,我们没有剩下的两个映射:
Notice the asymmetry between these two definitions. In general, we don’t have the two remaining mappings:
R ∘ L -> ID -- not necessarily
IC -> L ∘ R -- not necessarilyR ∘ L -> ID -- not necessarily
IC -> L ∘ R -- not necessarily
由于这种不对称性,函子L被称为函子 的左伴随R,而函子R是 的右伴随L。(当然,只有当您以特定方式绘制图表时,左和右才有意义。)
Because of this asymmetry, the functor L is called the left adjoint to the functor R, while the functor R is the right adjoint to L. (Of course, left and right make sense only if you draw your diagrams one particular way.)
附加词的紧凑表示法是:
The compact notation for the adjunction is:
L ⊣ RL ⊣ R
为了更好地理解附加词,让我们更详细地分析单位和计数单位。
To better understand the adjunction, let’s analyze the unit and the counit in more detail.
我们先从单位开始吧。这是一个自然变换,所以它是一个态射族。给定Dd中的一个对象, η 的分量是 之间的态射,它等于、 和;在图中,它被称为:I dd(R ∘ L) dd'
Let’s start with the unit. It’s a natural transformation, so it’s a family of morphisms. Given an object d in D, the component of η is a morphism between I d, which is equal to d, and (R ∘ L) d; which, in the picture, is called d':
ηd :: d -> (R ∘ L) dηd :: d -> (R ∘ L) d
请注意,该组合R∘L是D中的一个内函子。
Notice that the composition R∘L is an endofunctor in D.
这个方程告诉我们,我们可以选择Dd中的任何对象作为起点,并使用往返函子来选择目标对象。然后我们向目标射出一支箭——态射。R ∘ Ld'ηd
This equation tells us that we can pick any object d in D as our starting point, and use the round trip functor R ∘ L to pick our target object d'. Then we shoot an arrow — the morphism ηd — to our target.
同理,单位ε的分量可以描述为:
By the same token, the component of of the counit ε can be described as:
εc' :: (L ∘ R) c -> cεc' :: (L ∘ R) c -> c
哪里c'是(L ∘ R) c. 它告诉我们可以选择Cc中的任何对象作为目标,并使用往返函子来选择源。然后我们将箭——态射——从源射向目标。L ∘ Rc'εc'
where c' is (L ∘ R) c. It tells us that we can pick any object c in C as our target, and use the round trip functor L ∘ R to pick the source c'. Then we shoot the arrow — the morphism εc' — from the source to the target.
看待 unit 和 counit 的另一种方式是,unit 允许我们在任何可以在D上插入恒等函子的地方引入组合;counit 让我们消除组合,用C上的恒等式替换它。这导致了一些“明显”的一致性条件,确保引入和消除不会改变任何东西:R ∘ LL ∘ R
Another way of looking at unit and counit is that unit lets us introduce the composition R ∘ L anywhere we could insert an identity functor on D; and counit lets us eliminate the composition L ∘ R, replacing it with the identity on C. That leads to some “obvious” consistency conditions, which make sure that introduction followed by elimination doesn’t change anything:
L = L ∘ ID -> L ∘ R ∘ L -> IC ∘ L = LL = L ∘ ID -> L ∘ R ∘ L -> IC ∘ L = L
R = ID ∘ R -> R ∘ L ∘ R -> R ∘ IC = RR = ID ∘ R -> R ∘ L ∘ R -> R ∘ IC = R
这些被称为三角形恒等式,因为它们使下面的图可以交换:
These are called triangular identities because they make the following diagrams commute:
这些是函子范畴中的图:箭头是自然变换,它们的组合是自然变换的水平组合。在组件中,这些身份变为:
These are diagrams in the functor category: the arrows are natural transformations, and their composition is the horizontal composition of natural transformations. In components, these identities become:
ε L d ∘ L η d = id L d
R ε c ∘ η R c = id R cε L d ∘ L η d = id L d
R ε c ∘ η R c = id R c
我们经常在 Haskell 中以不同的名称看到单位和单位。单位被称为return(或pure,在 的定义中Applicative):
We often see unit and counit in Haskell under different names. Unit is known as return (or pure, in the definition of Applicative):
return :: d -> m dreturn :: d -> m d
并计算为extract:
and counint as extract:
extract :: w c -> cextract :: w c -> c
这里,m是对应于 的 (endo-) 函子R∘L,w是 对应于 的 (endo-) 函子L∘R。正如我们稍后将看到的,它们分别是 monad 和 comonad 定义的一部分。
Here, m is the (endo-) functor corresponding to R∘L, and w is the (endo-) functor corresponding to L∘R. As we’ll see later, they are part of the definition of a monad and a comonad, respectively.
如果将 endofunctor 视为容器,则该单元(或return)是一个多态函数,它在任意类型的值周围创建一个默认框。counit(或extract)执行相反的操作:它从容器中检索或生成单个值。
If you think of an endofunctor as a container, the unit (or return) is a polymorphic function that creates a default box around a value of arbitrary type. The counit (or extract) does the reverse: it retrieves or produces a single value from a container.
稍后我们会看到,每对伴随函子都定义了一个 monad 和一个 comonad。相反,每个单子或共单子都可以分解为一对伴随函子——不过,这种因式分解并不是唯一的。
We’ll see later that every pair of adjoint functors defines a monad and a comonad. Conversely, every monad or comonad may be factorized into a pair of adjoint functors — this factorization is not unique, though.
在 Haskell 中,我们大量使用 monad,但很少将它们分解为伴随函子对,主要是因为这些函子通常会让我们脱离Hask。
In Haskell, we use monads a lot, but only rarely factorize them into pairs of adjoint functors, primarily because those functors would normally take us out of Hask.
然而,我们可以在 Haskell 中定义内函子的附加词。以下是摘自的部分定义Data.Functor.Adjunction:
We can however define adjunctions of endofunctors in Haskell. Here’s part of the definition taken from Data.Functor.Adjunction:
class (Functor f, Representable u) =>
Adjunction f u | f -> u, u -> f where
unit :: a -> u (f a)
counit :: f (u a) -> a
class (Functor f, Representable u) =>
Adjunction f u | f -> u, u -> f where
unit :: a -> u (f a)
counit :: f (u a) -> a
这个定义需要一些解释。首先,它描述了一个多参数类型类——两个参数是f和u。Adjunction它在这两个类型构造函数之间建立了一种调用关系。
This definition requires some explanation. First of all, it describes a multi-parameter type class — the two parameters being f and u. It establishes a relation called Adjunction between these two type constructors.
竖线后面的附加条件指定功能依赖性。例如,f -> u意味着由(和之间的关系是一个函数,这里是类型构造函数)u决定。相反,意味着,如果我们知道,则是唯一确定的。ffuu -> fuf
Additional conditions, after the vertical bar, specify functional dependencies. For instance, f -> u means that u is determined by f (the relation between f and u is a function, here on type constructors). Conversely, u -> f means that, if we know u, then f is uniquely determined.
稍后我将解释为什么在 Haskell 中我们可以强加这样的条件:右伴随u是一个可表示函子。
I’ll explain in a moment why, in Haskell, we can impose the condition that the right adjoint u be a representable functor.
根据 hom 集的自然同构,存在一个等效的附加定义。这个定义与我们迄今为止所研究的通用结构很好地联系在一起。每当您听到存在某种独特的态射(它分解某些构造)的说法时,您都应该将其视为某些集合到本集的映射。这就是“选择一个独特的态射”的意义。
There is an equivalent definition of the adjunction in terms of natural isomorphisms of hom-sets. This definition ties nicely with universal constructions we’ve been studying so far. Every time you hear the statement that there is some unique morphism, which factorizes some construction, you should think of it as a mapping of some set to a hom-set. That’s the meaning of “picking a unique morphism.”
此外,因式分解通常可以用自然变换来描述。因式分解涉及交换图——某些态射等于两个态射(因子)的组合。自然变换将态射映射到通勤图。因此,在通用构造中,我们从态射到交换图,然后到唯一态射。我们最终得到从态射到态射,或者从一个本集到另一个本集(通常在不同范畴中)的映射。如果这个映射是可逆的,并且如果它可以自然地扩展到所有 hom-sets,我们就得到了一个附加。
Furthermore, factorization can be often described in terms of natural transformations. Factorization involves commuting diagrams — some morphism being equal to a composition of two morphisms (factors). A natural transformation maps morphisms to commuting diagrams. So, in a universal construction, we go from a morphism to a commuting diagram, and then to a unique morphism. We end up with a mapping from morphism to morphism, or from one hom-set to another (usually in different categories). If this mapping is invertible, and if it can be naturally extended across all hom-sets, we have an adjunction.
通用结构和附加词之间的主要区别在于,后者是针对所有 hom-set 进行全局定义的。例如,使用通用构造,您可以定义两个选定对象的乘积,即使该范畴中的任何其他对象对都不存在该乘积。正如我们很快就会看到的,如果任何一对对象的乘积存在于一个范畴中,它也可以通过附加来定义。
The main difference between universal constructions and adjunctions is that the latter are defined globally — for all hom-sets. For instance, using a universal construction you can define a product of two select objects, even if it doesn’t exist for any other pair of objects in that category. As we’ll see soon, if the product of any pair of objects exists in a category, it can be also defined through an adjunction.
这是使用 hom-sets 的附加定义。和以前一样,我们有两个函子L :: D->C和R :: C->D。我们选择两个任意对象: Dd中的源对象和C中的目标对象。我们可以使用将源对象映射到C。现在我们在C和中有两个对象。他们定义了一个 hom-set:cdLL dc
Here’s the alternative definition of the adjunction using hom-sets. As before, we have two functors L :: D->C and R :: C->D. We pick two arbitrary objects: the source object d in D, and the target object c in C. We can map the source object d to C using L. Now we have two objects in C, L d and c. They define a hom-set:
C(L d, c)C(L d, c)
c同样,我们可以使用 来映射目标对象R。现在我们在D和d中有两个对象R c。他们也定义了一个 hom 集:
Similarly, we can map the target object c using R. Now we have two objects in D, d and R c. They, too, define a hom set:
D(d, R c)D(d, R c)
我们说L是左伴随的,当且R仅当存在 hom 集的同构:
We say that L is left adjoint to R iff there is an isomorphism of hom sets:
C(L d, c) ≅ D(d, R c)C(L d, c) ≅ D(d, R c)
这在d和中都是很自然的c。
that is natural both in d and c.
自然性意味着源可以在Dd上平滑变化;和目标,穿过C。更准确地说,我们在以下两个(协变)函子之间有一个从C到Set 的自然转换。以下是这些函子对对象的作用:cφ
Naturality means that the source d can be varied smoothly across D; and the target c, across C. More precisely, we have a natural transformation φ between the following two (covariant) functors from C to Set. Here’s the action of these functors on objects:
c -> C(L d, c)
c -> D(d, R c)c -> C(L d, c)
c -> D(d, R c)
另一个自然变换 ,ψ在以下(逆变)函子之间起作用:
The other natural transformation, ψ, acts between the following (contravariant) functors:
d -> C(L d, c)
d -> D(d, R c)d -> C(L d, c)
d -> D(d, R c)
两种自然变换都必须是可逆的。
Both natural transformations must be invertible.
很容易证明附加词的两个定义是等价的。例如,让我们从 hom 集的同构出发推导单位变换:
It’s easy to show that the two definitions of the adjunction are equivalent. For instance, let’s derive the unit transformation starting from the isomorphism of hom-sets:
C(L d, c) ≅ D(d, R c)C(L d, c) ≅ D(d, R c)
由于这种同构适用于任何对象c,因此它也必须适用于c = L d:
Since this isomorphism works for any object c, it must also work for c = L d:
C(L d, L d) ≅ D(d, (R ∘ L) d)C(L d, L d) ≅ D(d, (R ∘ L) d)
我们知道左侧必须包含至少一个态射,即恒等式。自然变换将把这个态射映射到D(d, (R ∘ L) d)or 的一个元素,插入恒等函子I,一个态射:
We know that the left hand side must contain at least one morphism, the identity. The natural transformation will map this morphism to an element of D(d, (R ∘ L) d) or, inserting the identity functor I, a morphism in:
D(I d, (R ∘ L) d)D(I d, (R ∘ L) d)
我们得到一个由 参数化的态射族d。I它们在函子和函子之间形成自然变换R ∘ L(自然性条件很容易验证)。这正是我们的单位η。
We get a family of morphisms parameterized by d. They form a natural transformation between the functor I and the functor R ∘ L (the naturality condition is easy to verify). This is exactly our unit, η.
反过来,从单位和辅单位的存在性出发,我们可以定义hom集之间的变换。例如,让我们f在 hom-set 中选择一个任意态射C(L d, c)。我们想要定义一个φ,作用于f,产生 的态射D(d, R c)。
Conversely, starting from the existence of the unit and co-unit, we can define the transformations between hom-sets. For instance, let’s pick an arbitrary morphism f in the hom-set C(L d, c). We want to define a φ that, acting on f, produces a morphism in D(d, R c).
确实没有太多选择。我们可以尝试的一件事是f使用R. 这将产生一个R f从R (L d)到 的态射R c——一个作为 的元素的态射D((R ∘ L) d, R c)。
There isn’t really much choice. One thing we can try is to lift f using R. That will produce a morphism R f from R (L d) to R c — a morphism that’s an element of D((R ∘ L) d, R c).
我们需要 的一个组成部分是从到 的φ态射。这不是问题,因为我们可以使用 的组件从到。我们得到:dR cηdd(R ∘ L) d
What we need for a component of φ, is a morphism from d to R c. That’s not a problem, since we can use a component of ηd to get from d to (R ∘ L) d. We get:
φf = R f ∘ ηdφf = R f ∘ ηd
另一个方向类推, 的推导也是如此ψ。
The other direction is analogous, and so is the derivation of ψ.
回到 Haskell 的定义Adjunction,自然变换φ和ψ分别被多态(ina和b)函数leftAdjunct和替代rightAdjunct。函子L和R被称为fand u:
Going back to the Haskell definition of Adjunction, the natural transformations φ and ψ are replaced by polymorphic (in a and b) functions leftAdjunct and rightAdjunct, respectively. The functors L and R are called f and u:
class (Functor f, Representable u) =>
Adjunction f u | f -> u, u -> f where
leftAdjunct :: (f a -> b) -> (a -> u b)
rightAdjunct :: (a -> u b) -> (f a -> b)class (Functor f, Representable u) =>
Adjunction f u | f -> u, u -> f where
leftAdjunct :: (f a -> b) -> (a -> u b)
rightAdjunct :: (a -> u b) -> (f a -> b)
这些映射证明了unit/counit公式和leftAdjunct/公式之间的等价性:rightAdjunct
The equivalence between the unit/counit formulation and the leftAdjunct/rightAdjunct formulation is witnessed by these mappings:
unit = leftAdjunct id
counit = rightAdjunct id
leftAdjunct f = fmap f . unit
rightAdjunct f = counit . fmap funit = leftAdjunct id
counit = rightAdjunct id
leftAdjunct f = fmap f . unit
rightAdjunct f = counit . fmap f
遵循从附加词的分类描述到 Haskell 代码的翻译是非常有启发性的。我强烈鼓励将此作为一项练习。
It’s very instructive to follow the translation from the categorical description of the adjunction to Haskell code. I highly encourage this as an exercise.
我们现在准备解释为什么在 Haskell 中,右伴随自动成为可表示函子。这样做的原因是,对于第一个近似,我们可以将 Haskell 类型的范畴视为集合的范畴。
We are now ready to explain why, in Haskell, the right adjoint is automatically a representable functor. The reason for this is that, to the first approximation, we can treat the category of Haskell types as the category of sets.
当右范畴D是Set时,右伴随是从C到Set 的R函子。如果我们能在C中找到一个对象,使得 hom 函子自然同构于 ,则这样的函子是可表示的。事实证明, if是从Set到C的某个函子的右伴随,这样的对象总是存在 - 它是下单例集合的图像:repC(rep, _)RRL()L
When the right category D is Set, the right adjoint R is a functor from C to Set. Such a functor is representable if we can find an object rep in C such that the hom-functor C(rep, _) is naturally isomorphic to R. It turns out that, if R is the right adjoint of some functor L from Set to C, such an object always exists — it’s the image of the singleton set () under L:
rep = L ()rep = L ()
事实上,附加词告诉我们以下两个 hom 集自然是同构的:
Indeed, the adjunction tells us that the following two hom-sets are naturally isomorphic:
C(L (), c) ≅ Set((), R c)C(L (), c) ≅ Set((), R c)
对于给定的c,右侧是从单例集合到 的函数()集R c。我们之前已经看到,每个这样的函数都会从集合中选择一个元素R c。此类函数的集合与集合同构R c。所以我们有:
For a given c, the right hand side is the set of functions from the singleton set () to R c. We’ve seen earlier that each such function picks one element from the set R c. The set of such functions is isomorphic to the set R c. So we have:
C(L (), -) ≅ RC(L (), -) ≅ R
这说明R确实是有代表性的。
which shows that R is indeed representable.
我们之前已经介绍了几个使用通用结构的概念。其中许多概念在全局定义时更容易使用附加词来表达。最简单的例子就是产品。乘积的通用构造的要点是能够通过通用乘积分解任何类似乘积的候选者。
We have previously introduced several concepts using universal constructions. Many of those concepts, when defined globally, are easier to express using adjunctions. The simplest non-trivial example is that of the product. The gist of the universal construction of the product is the ability to factorize any product-like candidate through the universal product.
更准确地说,两个对象a和的乘积是配备有两个态射的b对象(a × b)(或用 Haskell 表示法) ,因此,对于配备有两个态射和的任何其他候选对象,存在一个通过和因式分解和的唯一态射。(a, b)fstsndcp::c->aq::c->bm::c->(a, b)pqfstsnd
More precisely, the product of two objects a and b is the object (a × b) (or (a, b) in the Haskell notation) equipped with two morphisms fst and snd such that, for any other candidate c equipped with two morphisms p::c->a and q::c->b, there exists a unique morphism m::c->(a, b) that factorizes p and q through fst and snd.
正如我们之前所看到的,在 Haskell 中,我们可以实现factorizer从两个投影生成此态射的 a :
As we’ve seen earlier, in Haskell, we can implement a factorizer that generates this morphism from the two projections:
factorizer :: (c -> a) -> (c -> b) -> (c -> (a, b))
factorizer p q = \x -> (p x, q x)factorizer :: (c -> a) -> (c -> b) -> (c -> (a, b))
factorizer p q = \x -> (p x, q x)
很容易验证因式分解条件是否成立:
It’s easy to verify that the factorization conditions hold:
fst . factorizer p q = p
snd . factorizer p q = qfst . factorizer p q = p
snd . factorizer p q = q
我们有一个映射,它接受一对态射p并q产生另一个态射m = factorizer p q。
We have a mapping that takes a pair of morphisms p and q and produces another morphism m = factorizer p q.
我们如何将其转换为我们需要定义附加的两个 hom-set 之间的映射?诀窍是走出Hask,将这对态射视为产品范畴中的单个态射。
How can we translate this into a mapping between two hom-sets that we need to define an adjunction? The trick is to go outside of Hask and treat the pair of morphisms as a single morphism in the product category.
让我提醒您什么是产品范畴。取两个任意范畴C和D。产品范畴C×D中的对象是一对对象,一个来自C,一个来自D。态射是一对态射,一个来自C,一个来自D。
Let me remind you what a product category is. Take two arbitrary categories C and D. The objects in the product category C×D are pairs of objects, one from C and one from D. The morphisms are pairs of morphisms, one from C and one from D.
要定义某个范畴C中的产品,我们应该从产品范畴C×C开始。C中的成对态射是乘积范畴C×C中的单态射。
To define a product in some category C, we should start with the product category C×C. Pairs of morphism from C are single morphisms in the product category C×C.
一开始我们可能会有点困惑,因为我们使用产品范畴来定义产品。然而,这些是非常不同的产品。我们不需要通用的结构来定义产品范畴。我们所需要的只是一对对象和一对态射的概念。
It might be a little confusing at first that we are using a product category to define a product. These are, however, very different products. We don’t need a universal construction to define a product category. All we need is the notion of a pair of objects and a pair of morphisms.
然而, C中的一对对象并不是C中的对象。它是一个不同范畴的对象,C×C。我们可以将这对正式地写为,其中和是C的对象。另一方面,为了定义对象(或在 Haskell 中),通用构造是必要的,该对象是同一范畴C中的对象。该对象应该以通用构造指定的方式表示该对。它并不总是存在,即使对于某些对象存在,对于C中的其他对象对也可能不存在。<a, b>aba×b(a, b)<a, b>
However, a pair of objects from C is not an object in C. It’s an object in a different category, C×C. We can write the pair formally as <a, b>, where a and b are objects of C. The universal construction, on the other hand, is necessary in order to define the object a×b (or (a, b) in Haskell), which is an object in the same category C. This object is supposed to represent the pair <a, b> in a way specified by the universal construction. It doesn’t always exist and, even if it exists for some, might not exist for other pairs of objects in C.
现在让我们将 视为factorizerhom-sets 的映射。第一个 hom-set 位于产品范畴C×C中,第二个位于C中。C×C中的一般态射是一对态射<f, g>:
Let’s now look at the factorizer as a mapping of hom-sets. The first hom-set is in the product category C×C, and the second is in C. A general morphism in C×C would be a pair of morphisms <f, g>:
f :: c' -> a
g :: c'' -> bf :: c' -> a
g :: c'' -> b
与c''可能不同c'。但为了定义一个乘积,我们对C×C中的特殊态射感兴趣,这对p和q共享相同的源对象c。没关系:在附加的定义中,左 hom-set 的源不是任意对象 - 它是左函子L作用于右范畴中的某个对象的结果。符合要求的函子很容易猜到 - 它是从C到C×C 的对角函子,其对对象的作用是:
with c'' potentially different from c'. But to define a product, we are interested in a special morphism in C×C, the pair p and q that share the same source object c. That’s okay: In the definition of an adjuncion, the source of the left hom-set is not an arbitrary object — it’s the result of the left functor L acting on some object from the right category. The functor that fits the bill is easy to guess — it’s the diagonal functor from C to C×C, whose action on objects is:
Δ c = <c, c>Δ c = <c, c>
因此,我们的附加中的左侧 hom-set 应该是:
The left-hand side hom-set in our adjunction should thus be:
(C×C)(Δ c, <a, b>)(C×C)(Δ c, <a, b>)
它是产品范畴中的一款 hom-set。它的元素是态射对,我们将其视为 的参数factorizer:
It’s a hom-set in the product category. Its elements are pairs of morphisms that we recognize as the arguments to our factorizer:
(c -> a) -> (c -> b) ...(c -> a) -> (c -> b) ...
右侧 hom-set 位于C中,它位于源对象和作用于C×C中目标对象c的某个函子的结果之间。这是将这对映射到我们的产品对象的函子。我们将 hom-set 的这个元素识别为的结果:R<a, b>a×bfactorizer
The right-hand side hom-set lives in C, and it goes between the source object c and the result of some functor R acting on the target object in C×C. That’s the functor that maps the pair <a, b> to our product object, a×b. We recognize this element of the hom-set as the result of the factorizer:
... -> (c -> (a, b))... -> (c -> (a, b))
我们还没有完整的附属。为此,我们首先需要我们factorizer是可逆的——我们正在hom 集之间建立同构。的逆factorizer应该从态射开始——从某个对象到乘积对象的m态射。换句话说,应该是以下元素:ca×bm
We still don’t have a full adjunction. For that we first need our factorizer to be invertible — we are building an isomorphism between hom-sets. The inverse of the factorizer should start from a morphism m — a morphism from some object c to the product object a×b. In other words, m should be an element of:
C(c, a×b)C(c, a×b)
逆因式分解器应该映射到C×C中从到 的m态射;换句话说,态射是以下元素的一个元素:<p, q><c, c><a, b>
The inverse factorizer should map m to a morphism <p, q> in C×C that goes from <c, c> to <a, b>; in other words, a morphism that’s an element of:
(C×C)(Δ c, <a, b>)(C×C)(Δ c, <a, b>)
如果该映射存在,我们就得出结论,存在对角函子的右伴随。该函子定义了一个产品。
If that mapping exists, we conclude that there exists the right adjoint to the diagonal functor. That functor defines a product.
factorizer在 Haskell 中,我们总是可以通过m分别与fst和组合来构造 的逆snd。
In Haskell, we can always construct the inverse of the factorizer by composing m with, respectively, fst and snd.
p = fst ∘ m
q = snd ∘ mp = fst ∘ m
q = snd ∘ m
为了完成定义乘积的两种方法的等价性证明,我们还需要证明 hom 集之间的映射在 、 和 中是a自然b的c。我将把这个作为练习留给专门的读者。
To complete the proof of the equivalence of the two ways of defining a product we also need to show that the mapping between hom-sets is natural in a, b, and c. I will leave this as an exercise for the dedicated reader.
总结我们所做的:分类积可以全局定义为对角函子的右伴随:
To summarize what we have done: A categorical product may be defined globally as the right adjoint of the diagonal functor:
(C × C)(Δ c, <a, b>) ≅ C(c, a×b)(C × C)(Δ c, <a, b>) ≅ C(c, a×b)
这里,是我们的右伴随函子a×b对 的作用的结果。请注意, C×C中的任何函子都是双函子,双函子也是如此。在 Haskell 中,双函子简单地写为。您可以将其应用于两种类型并获取它们的产品类型,例如:Product<a, b>ProductProduct(,)
Here, a×b is the result of the action of our right adjoint functor Product on the pair <a, b>. Notice that any functor from C×C is a bifunctor, so Product is a bifunctor. In Haskell, the Product bifunctor is written simply as (,). You can apply it to two types and get their product type, for instance:
(,) Int Bool ~ (Int, Bool)(,) Int Bool ~ (Int, Bool)
指数ba或函数对象可以使用通用构造a⇒b来定义。如果该结构对于所有对象对都存在,则可以将其视为附加项。再次强调,诀窍是集中注意力在下面的陈述上:
The exponential ba, or the function object a⇒b, can be defined using a universal construction. This construction, if it exists for all pairs of objects, can be seen as an adjunction. Again, the trick is to concentrate on the statement:
对于任何其他
z具有态射的对象g :: z × a -> b存在唯一的态射
h :: z -> (a⇒b)
For any other object
zwith a morphismg :: z × a -> bthere is a unique morphism
h :: z -> (a⇒b)
该语句建立了 hom-set 之间的映射。
This statement establishes a mapping between hom-sets.
在这种情况下,我们处理的是同一范畴的对象,因此两个伴随函子是内函子。左(内)函子L作用于对象 时z,产生z × a。它是一个函子,对应于采用某个固定的乘积a。
In this case, we are dealing with objects in the same category, so the two adjoint functors are endofunctors. The left (endo-)functor L, when acting on object z, produces z × a. It’s a functor that corresponds to taking a product with some fixed a.
右(内)函子R作用于 时b产生函数对象a⇒b(或ba)。再次,a是固定的。这两个函子之间的连接通常写为:
The right (endo-)functor R, when acting on b produces the function object a⇒b (or ba). Again, a is fixed. The adjunction between these two functors is often written as:
- × a ⊣ (-)a- × a ⊣ (-)a
通过重新绘制我们在通用构造中使用的图表,可以最好地看到作为该附加项基础的 hom 集的映射。
The mapping of hom-sets that underlies this adjunction is best seen by redrawing the diagram that we used in the universal construction.
请注意,eval态射只不过是这个附加词的计数单位:
Notice that the eval morphism is nothing else but the counit of this adjunction:
(a⇒b) × a -> b(a⇒b) × a -> b
在哪里:
where:
(a⇒b) × a = (L ∘ R) b(a⇒b) × a = (L ∘ R) b
我之前提到过,通用构造定义了一个唯一的对象,直到同构。这就是为什么我们有“那个”乘积和“那个”指数。此属性也可转换为伴随性:如果函子具有伴随性,则该伴随性在同构上是唯一的。
I have previously mentioned that a universal construction defines a unique object, up to isomorphism. That’s why we have “the” product and “the” exponential. This property translates to adjunctions as well: if a functor has an adjoint, this adjoint is unique up to isomorphism.
导出 的自然性平方ψ,即两个(逆变)函子之间的变换:
a -> C(L a, b)
a -> D(a, R b)Derive the naturality square for ψ, the transformation between the two (contravariant) functors:
a -> C(L a, b)
a -> D(a, R b)ε从附加词的第二个定义中的 hom 集同构开始导出计数。ε starting from the hom-sets isomorphism in the second definition of the adjunction.我要感谢 Edward Kmett 和 Gershom Bazerman 检查我的数学和逻辑,以及 André van Meulebrouck,他在这一系列帖子中自愿提供编辑帮助。
I’d like to thank Edward Kmett and Gershom Bazerman for checking my math and logic, and André van Meulebrouck, who has been volunteering his editing help throughout this series of posts.
自由结构是附加词的有力应用。自由函子被定义为健忘函子的左伴随。健忘的函子通常是一个非常简单的函子,它忘记了一些结构。例如,许多有趣的范畴都是建立在集合之上的。但是抽象这些集合的分类对象没有内部结构——它们没有元素。尽管如此,这些对象通常带有集合的记忆,从某种意义上说,存在从给定范畴C到Set的映射(函子)。C中某个对象对应的集合称为其底层集合。
Free constructions are a powerful application of adjunctions. A free functor is defined as the left adjoint to a forgetful functor. A forgetful functor is usually a pretty simple functor that forgets some structure. For instance, lots of interesting categories are built on top of sets. But categorical objects, which abstract those sets, have no internal structure — they have no elements. Still, those objects often carry the memory of sets, in the sense that there is a mapping — a functor — from a given category C to Set. A set corresponding to some object in C is called its underlying set.
幺半群是具有基础集合(元素集合)的对象。从幺半群MonU范畴到集合范畴,有一个健忘函子,它将幺半群映射到其底层集合。它将幺半群态射(同态)映射到集合之间的函数。
Monoids are such objects that have underlying sets — sets of elements. There is a forgetful functor U from the category of monoids Mon to the category of sets, which maps monoids to their underlying sets. It also maps monoid morphisms (homomorphisms) to functions between sets.
我喜欢认为Mon有人格分裂。一方面,它是一堆带有乘法和单位元素的集合。另一方面,它是一个没有特征的对象的范畴,其唯一的结构被编码在它们之间的态射中。每个保留乘法和单位的集合函数都会在Mon中产生态射。
I like to think of Mon as having split personality. On the one hand, it’s a bunch of sets with multiplication and unit elements. On the other hand, it’s a category with featureless objects whose only structure is encoded in morphisms that go between them. Every set-function that preserves multiplication and unit gives rise to a morphism in Mon.
要记住的事情:
Things to keep in mind:
幺半群 m 1和 m 2具有相同的基础集。m 2和 m 3的基础集合之间的函数数量多于它们之间的态射数量。
Monoids m1 and m2 have the same underlying set. There are more functions between the underlying sets of m2 and m3 than there are morphisms between them.
F健忘函子的左伴随函子U是自由函子,它从生成器集构建自由幺半群。该附加项源自我们之前讨论过的自由幺半群通用构造。
The functor F that’s the left adjoint to the forgetful functor U is the free functor that builds free monoids from their generator sets. The adjunction follows from the free monoid universal construction we’ve discussed before.
就 hom 集而言,我们可以将这个附加语句写为:
In terms of hom-sets, we can write this adjunction as:
Mon(F x, m) ≅ Set(x, U m)Mon(F x, m) ≅ Set(x, U m)
这种(自然的x和m)同构告诉我们:
This (natural in x and m) isomorphism tells us that:
F x对于生成的自由幺半群x和任意幺半群之间的每个幺半群同态m,都有一个独特的函数将生成器集合嵌入x到 的底层集合中m。这是 中的一个函数Set(x, U m)。F x generated by x and an arbitrary monoid m there is a unique function that embeds the set of generators x in the underlying set of m. It’s a function in Set(x, U m).x某些函数的底层集合中的每个函数,在由 生成的自由幺半群和 幺半群m之间存在唯一的幺半群态射。(这就是我们在普遍构造中所说的态射。)xmhx in the underlying set of some m there is a unique monoid morphism between the free monoid generated by x and the monoid m. (This is the morphism we called h in our universal construction.)直觉是 是F x可以在 的基础上构建的“最大”幺半群x。如果我们可以观察幺半群的内部,我们会发现任何属于 的态射都会将这个自由幺半群Mon(F x, m) 嵌入到其他一些幺半群中m。它通过可能识别一些元素来做到这一点。特别是,它将 的生成器F x(即 的元素x)嵌入到 中m。该附加表明 的嵌入(由右侧的x函数给出)唯一地确定了左侧幺半群的嵌入,反之亦然。Set(x, U m)
The intuition is that F x is the “maximum” monoid that can be built on the basis of x. If we could look inside monoids, we would see that any morphism that belongs to Mon(F x, m) embeds this free monoid in some other monoid m. It does it by possibly identifying some elements. In particular, it embeds the generators of F x (i.e., the elements of x) in m. The adjunction shows that the embedding of x, which is given by a function from Set(x, U m) on the right, uniquely determines the embedding of monoids on the left, and vice versa.
在 Haskell 中,列表数据结构是一个自由的幺半群(有一些注意事项:请参阅Dan Doel 的博客文章)。列表类型[a]是一个自由幺半群,其类型a表示生成器集合。例如,该类型[Char]包含单位元素(空列表)[]和单例,例如['a'], ['b']- 自由幺半群的生成器。其余部分是通过应用“产品”生成的。在这里,两个列表的乘积只是将一个列表附加到另一个列表。追加是关联的和统一的(也就是说,有一个中性元素——这里是空列表)。由 生成的自由幺半群Char不过是 中所有字符串的集合Char。它String在 Haskell 中被称为:
In Haskell, the list data structure is a free monoid (with some caveats: see Dan Doel’s blog post). A list type [a] is a free monoid with the type a representing the set of generators. For instance, the type [Char] contains the unit element — the empty list [] — and the singletons like ['a'], ['b'] — the generators of the free monoid. The rest is generated by applying the “product.” Here, the product of two lists simply appends one to another. Appending is associative and unital (that is, there is a neutral element — here, the empty list). A free monoid generated by Char is nothing but the set of all strings of characters from Char. It’s called String in Haskell:
type String = [Char]type String = [Char]
(type定义类型同义词——现有类型的不同名称)。
(type defines a type synonym — a different name for an existing type).
另一个有趣的例子是仅由一个生成器构建的免费幺半群。它是单位列表的类型,[()]。它的元素是[]、[()]、[(), ()]等。每个这样的列表都可以用一个自然数(它的长度)来描述。单位列表中没有更多信息编码。附加两个这样的列表会产生一个新列表,其长度是其组成部分的长度之和。很容易看出该类型[()]与自然数(零)的加法幺半群同构。下面是两个互逆的函数,见证了这种同构:
Another interesting example is a free monoid built from just one generator. It’s the type of the list of units, [()]. Its elements are [], [()], [(), ()], etc. Every such list can be described by one natural number — its length. There is no more information encoded in the list of units. Appending two such lists produces a new list whose length is the sum of the lengths of its constituents. It’s easy to see that the type [()] is isomorphic to the additive monoid of natural numbers (with zero). Here are the two functions that are the inverse of each other, witnessing this isomorphism:
toNat :: [()] -> Int
toNat = length
toLst :: Int -> [()]
toLst n = replicate n ()toNat :: [()] -> Int
toNat = length
toLst :: Int -> [()]
toLst n = replicate n ()
为了简单起见,我使用了 typeInt而不是Natural,但想法是相同的。该函数创建一个预先填充给定值(此处为单位)replicate的长度列表。n
For simplicity I used the type Int rather than Natural, but the idea is the same. The function replicate creates a list of length n pre-filled with a given value — here, the unit.
接下来是一些挥手的论点。这类论证远非严格,但它们有助于形成直觉。
What follows are some hand-waving arguments. Those kind of arguments are far from rigorous, but they help in forming intuitions.
为了获得有关自由/遗忘附加词的一些直觉,请记住函子和函数本质上是有损的。函子可以折叠多个对象和态射,函数可以将集合的多个元素聚集在一起。此外,它们的图像可能仅覆盖其共域的一部分。
To get some intuition about the free/forgetful adjunctions it helps to keep in mind that functors and functions are lossy in nature. Functors may collapse multiple objects and morphisms, functions may bunch together multiple elements of a set. Also, their image may cover only part of their codomain.
Set中的“平均”hom-set将包含一整套函数,从损耗最小的函数(例如,注入或可能的同构)开始,到将整个域折叠为单个元素的常数函数结束(如果有一个)。
An “average” hom-set in Set will contain a whole spectrum of functions starting with the ones that are least lossy (e.g., injections or, possibly, isomorphisms) and ending with constant functions that collapse the whole domain to a single element (if there is one).
我倾向于认为任意范畴中的态射也是有损的。这只是一种心理模型,但它是一个有用的模型,特别是在考虑附加词时 - 特别是那些其中一个范畴是Set的附加词。
I tend to think of morphisms in an arbitrary category as being lossy too. It’s just a mental model, but it’s a useful one, especially when thinking of adjunctions — in particular those in which one of the categories is Set.
形式上,我们只能谈论可逆(同构)或不可逆的态射。后一种可能被认为是有损的。还有单射和外射的概念,它概括了单射(非折叠)和满射(覆盖整个共域)函数的概念,但是有可能有一个同时是单射和外射的态射,并且是仍然是不可逆的。
Formally, we can only speak of morphisms that are invertible (isomorphisms) or non-invertible. It’s that latter kind that may be though of as lossy. There is also a notion of mono- and epi- morphisms that generalize the idea of injective (non-collapsing) and surjective (covering the whole codomain) functions, but it’s possible to have a morphism that is both mono and epi, and which is still non-invertible.
在“自由”⊣“健忘”附加词中,左侧有更多约束的范畴C ,右侧有较少约束的范畴D。C中的态射“较少”,因为它们必须保留一些额外的结构。对于Mon来说,他们必须保留乘法和单位。D中的态射不必保留尽可能多的结构,因此它们有“更多”。
In the Free ⊣ Forgetful adjunction, we have the more constrained category C on the left, and a less constrained category D on the right. Morphisms in C are “fewer” because they have to preserve some additional structure. In the case of Mon, they have to preserve multiplication and unit. Morphisms in D don’t have to preserve as much structure, so there are “more” of them.
当我们将健忘函子应用于C中的U对象时,我们认为它揭示了 的“内部结构” 。事实上,如果D是Set,我们认为定义了其底层集合的内部结构。(在任意范畴中,除了通过对象与其他对象的连接之外,我们不能谈论对象的内部结构,但在这里我们只是挥手。)ccUc
When we apply a forgetful functor U to an object c in C, we think of it as revealing the “internal structure” of c. In fact, if D is Set we think of U as defining the internal structure of c — its underlying set. (In an arbitrary category, we can’t talk about the internals of an object other than through its connections to other objects, but here we are just hand-waving.)
如果我们映射两个对象c'并c使用U,我们预计,一般来说,hom-set 的映射C(c', c)将仅覆盖 的子集D(U c', U c)。这是因为 中的态射C(c', c)必须保留额外的结构,而 中的态射则D(U c', U c)不需要。
If we map two objects c' and c using U, we expect that, in general, the mapping of the hom-set C(c', c) will cover only a subset of D(U c', U c). That’s because morphisms in C(c', c) have to preserve the additional structure, whereas the ones in D(U c', U c) don’t.
但由于附加词被定义为特定同构的同构,我们必须对 的选择非常挑剔c'。在附加项中,不是从Cc'中的任何位置选取的,而是从自由函子的(可能较小的)图像中选取的:F
But since an adjunction is defined as an isomporphism of particular hom-sets, we have to be very picky with our selection of c'. In the adjunction, c' is picked not from just anywhere in C, but from the (presumably smaller) image of the free functor F:
C(F d, c) ≅ D(d, U c)C(F d, c) ≅ D(d, U c)
因此的图像F必须由具有大量任意 的态射的对象组成c。F d事实上,从到 的结构保持态射必须与从到c的非结构保持态射一样多。这意味着 的图像必须由本质上无结构的对象组成(这样态射就没有可以保留的结构)。这种“无结构”的对象称为自由对象。dU cF
The image of F must therefore consist of objects that have lots of morphisms going to an arbitrary c. In fact, there has to be as many structure-preserving morphisms from F d to c as there are non-structure preserving morphisms from d to U c. It means that the image of F must consist of essentially structure-free objects (so that there is no structure to preserve by morphisms). Such “structure-free” objects are called free objects.
在幺半群的例子中,自由幺半群除了由单位定律和结合律生成的结构外没有其他结构。除此之外,所有乘法都会产生全新的元素。
In the monoid example, a free monoid has no structure other than what’s generated by unit and associativity laws. Other than that, all multiplications produce brand new elements.
在自由幺半群中,2*3 不是 6 — 它是一个新元素 [2, 3]。由于 [2, 3] 和 6 无法识别,因此m允许从该自由幺半群到任何其他幺半群的态射分别映射它们。但它也可以将 [2, 3] 和 6 (它们的乘积)映射到 的同一元素m。或者在加法幺半群中识别 [2, 3] 和 5(它们的和),等等。不同的标识给你不同的幺半群。
In a free monoid, 2*3 is not 6 — it’s a new element [2, 3]. Since there is no identification of [2, 3] and 6, a morphism from this free monoid to any other monoid m is allowed to map them separately. But it’s also okay for it to map both [2, 3] and 6 (their product) to the same element of m. Or to identify [2, 3] and 5 (their sum) in an additive monoid, and so on. Different identifications give you different monoids.
这导致了另一个有趣的直觉:自由幺半群,而不是执行幺半群运算,而是累积传递给它的参数。他们不是将 2 和 3 相乘,而是将 2 和 3 记在一个列表中。这种方案的优点是我们不必指定我们将使用什么幺半群运算。我们可以不断累积参数,并且只有在最后才对结果应用运算符。然后我们就可以选择要应用的运算符。我们可以将数字相加,或相乘,或执行加法模 2,等等。自由幺半群将表达式的创建与其求值分开。当我们谈论代数时,我们会再次看到这个想法。
This leads to another interesting intuition: Free monoids, instead of performing the monoidal operation, accumulate the arguments that were passed to it. Instead of multiplying 2 and 3 they remember 2 and 3 in a list. The advantage of this scheme is that we don’t have to specify what monoidal operation we will use. We can keep accumulating arguments, and only at the end apply an operator to the result. And it’s then that we can chose what operator to apply. We can add the numbers, or multiply them, or perform addition modulo 2, and so on. A free monoid separates the creation of an expression from its evaluation. We’ll see this idea again when we talk about algebras.
这种直觉可以推广到其他更复杂的自由结构。例如,我们可以在评估整个表达式树之前累积它们。这种方法的优点是我们可以转换这样的树以使评估更快或更少的内存消耗。例如,这是在实现矩阵演算时完成的,其中急切的求值将导致大量分配临时数组来存储中间结果。
This intuition generalizes to other, more elaborate free constructions. For instance, we can accumulate whole expression trees before evaluating them. The advantage of this approach is that we can transform such trees to make the evaluation faster or less memory consuming. This is, for instance, done in implementing matrix calculus, where eager evaluation would lead to lots of allocations of temporary arrays to store intermediate results.
m证明从这个自由幺半群 到任何幺半群 的态射以及从单例集合到 的底层集合的函数之间存在一一对应关系m。m, and functions from the singleton set to the underlying set of m.我要感谢 Gershom Bazerman 检查我的数学和逻辑,以及 André van Meulebrouck,他在这一系列帖子中自愿提供编辑帮助。
I’d like to thank Gershom Bazerman for checking my math and logic, and André van Meulebrouck, who has been volunteering his editing help throughout this series of posts.
程序员围绕单子开发了一整套神话。它应该是编程中最抽象和最困难的概念之一。有些人“明白了”,有些人则没有。对于许多人来说,当他们理解单子概念的那一刻就像一种神秘的体验。单子抽象了如此多不同结构的本质,以至于我们在日常生活中根本没有一个很好的类比。我们只能在黑暗中摸索,就像那些盲人摸着大象末端的不同部位,得意地喊道:“这是一根绳子”,“这是一根树干”,或者“这是一个卷饼!”
Programmers have developed a whole mythology around monads. It’s supposed to be one of the most abstract and difficult concepts in programming. There are people who “get it” and those who don’t. For many, the moment when they understand the concept of the monad is like a mystical experience. The monad abstracts the essence of so many diverse constructions that we simply don’t have a good analogy for it in everyday life. We are reduced to groping in the dark, like those blind men touching different parts of the elephant end exclaiming triumphantly: “It’s a rope,” “It’s a tree trunk,” or “It’s a burrito!”
让我澄清一下:围绕单子的整个神秘主义是误解的结果。单子是一个非常简单的概念。正是单子应用的多样性造成了混乱。
Let me set the record straight: The whole mysticism around the monad is the result of a misunderstanding. The monad is a very simple concept. It’s the diversity of applications of the monad that causes the confusion.
作为本文研究的一部分,我查找了管道胶带(又名鸭胶带)及其应用。以下是您可以用它做的一些事情的示例:
As part of research for this post I looked up duct tape (a.k.a., duck tape) and its applications. Here’s a little sample of things that you can do with it:
现在想象一下,您不知道什么是管道胶带,并且您试图根据此列表找出它。祝你好运!
Now imagine that you didn’t know what duct tape was and you were trying to figure it out based on this list. Good luck!
所以我想在“单子就像……”的陈词滥调中再添加一项:单子就像胶带。它的应用广泛多样,但其原理非常简单:它将事物粘合在一起。更准确地说,它组成事物。
So I’d like to add one more item to the collection of “the monad is like…” clichés: The monad is like duct tape. Its applications are widely diverse, but its principle is very simple: it glues things together. More precisely, it composes things.
这部分解释了许多程序员,尤其是来自命令式背景的程序员,在理解 monad 时遇到的困难。问题在于我们不习惯从函数组合的角度来思考编程。这是可以理解的。我们经常给中间值命名,而不是直接在函数之间传递它们。我们还内联短片段的粘合代码,而不是将它们抽象为辅助函数。下面是 C 语言中向量长度函数的命令式实现:
This partially explains the difficulties a lot of programmers, especially those coming from the imperative background, have with understanding the monad. The problem is that we are not used to thinking of programing in terms of function composition. This is understandable. We often give names to intermediate values rather than pass them directly from function to function. We also inline short segments of glue code rather than abstract them into helper functions. Here’s an imperative-style implementation of the vector-length function in C:
double vlen(double * v) {
double d = 0.0;
int n;
for (n = 0; n < 3; ++n)
d += v[n] * v[n];
return sqrt(d);
}double vlen(double * v) {
double d = 0.0;
int n;
for (n = 0; n < 3; ++n)
d += v[n] * v[n];
return sqrt(d);
}
将此与使函数组合明确的(程式化)Haskell 版本进行比较:
Compare this with the (stylized) Haskell version that makes function composition explicit:
vlen = sqrt . sum . fmap (flip (^) 2)vlen = sqrt . sum . fmap (flip (^) 2)
(^)(在这里,为了让事情变得更加神秘,我通过将第二个参数设置为 来部分应用求幂运算符2。)
(Here, to make things even more cryptic, I partially applied the exponentiation operator (^) by setting its second argument to 2.)
我并不是说 Haskell 的无点风格总是更好,只是函数组合是我们在编程中所做的一切的基础。即使我们有效地组合函数,Haskell 仍然不遗余力地提供命令式语法,称为一do元组合符号。我们稍后会看到它的用途。但首先,让我解释一下为什么我们首先需要单子组合。
I’m not arguing that Haskell’s point-free style is always better, just that function composition is at the bottom of everything we do in programming. And even though we are effectively composing functions, Haskell does go to great lengths to provide imperative-style syntax called the do notation for monadic composition. We’ll see its use later. But first, let me explain why we need monadic composition in the first place.
我们之前已经通过修饰常规函数得到了writer monad 。特定的修饰是通过将它们的返回值与字符串或者更一般地与幺半群的元素配对来完成的。我们现在可以认识到这种修饰是一个函子:
We have previously arrived at the writer monad by embellishing regular functions. The particular embellishment was done by pairing their return values with strings or, more generally, with elements of a monoid. We can now recognize that such embellishment is a functor:
newtype Writer w a = Writer (a, w)
instance Functor (Writer w) where
fmap f (Writer (a, w)) = Writer (f a, w)newtype Writer w a = Writer (a, w)
instance Functor (Writer w) where
fmap f (Writer (a, w)) = Writer (f a, w)
随后我们找到了一种组合修饰函数或克莱斯利箭头的方法,它们是以下形式的函数:
We have subsequently found a way of composing embellished functions, or Kleisli arrows, which are functions of the form:
a -> Writer w ba -> Writer w b
我们正是在这个组合内部实现了日志的积累。
It was inside the composition that we implemented the accumulation of the log.
我们现在准备好对 Kleisli 范畴进行更一般的定义。我们从范畴C和 endofunctor开始m。对应的克莱斯利范畴K与C具有相同的对象,但其态射不同。两个对象之间的态射a和Kb中的态射被实现为态射:
We are now ready for a more general definition of the Kleisli category. We start with a category C and an endofunctor m. The corresponding Kleisli category K has the same objects as C, but its morphisms are different. A morphism between two objects a and b in K is implemented as a morphism:
a -> m ba -> m b
在原来的C类中。重要的是要记住,我们将Ka中的克莱斯利箭头视为和 之间的态射b,而不是a和之间的态射m b。
in the original category C. It’s important to keep in mind that we treat a Kleisli arrow in K as a morphism between a and b, and not between a and m b.
在我们的示例中,m专门用于Writer w, 对于某些固定的幺半群w。
In our example, m was specialized to Writer w, for some fixed monoid w.
仅当我们能为克莱斯利箭头定义适当的组合时,它们才形成一个范畴。如果存在一个组合,它是关联的并且每个对象都有一个恒等箭头,则函子m称为monad,并且生成的范畴称为 Kleisli 范畴。
Kleisli arrows form a category only if we can define proper composition for them. If there is a composition, which is associative and has an identity arrow for every object, then the functor m is called a monad, and the resulting category is called the Kleisli category.
在 Haskell 中,Kleisli 组合是使用 Fish 运算符 定义的>=>,恒等箭头是一个名为 的多态函数return。这是使用 Kleisli 组合的 monad 的定义:
In Haskell, Kleisli composition is defined using the fish operator >=>, and the identity arrrow is a polymorphic function called return. Here’s the definition of a monad using Kleisli composition:
class Monad m where
(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c)
return :: a -> m aclass Monad m where
(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c)
return :: a -> m a
请记住,定义 monad 有许多等效的方法,并且这不是 Haskell 生态系统中的主要方法。我喜欢它的概念简单性和它提供的直观性,但还有其他定义在编程时更方便。我们稍后会讨论它们。
Keep in mind that there are many equivalent ways of defining a monad, and that this is not the primary one in the Haskell ecosystem. I like it for its conceptual simplicity and the intuition it provides, but there are other definitions that are more convenient when programming. We’ll talk about them momentarily.
在这个表述中,单子定律很容易表达。它们不能在 Haskell 中强制执行,但可以用于等式推理。它们只是 Kleisli 范畴的标准组成定律:
In this formulation, monad laws are very easy to express. They cannot be enforced in Haskell, but they can be used for equational reasoning. They are simply the standard composition laws for the Kleisli category:
(f >=> g) >=> h = f >=> (g >=> h) -- associativity
return >=> f = f -- left unit
f >=> return = f -- right unit(f >=> g) >=> h = f >=> (g >=> h) -- associativity
return >=> f = f -- left unit
f >=> return = f -- right unit
这种定义也表达了 monad 的真正含义:它是一种组合修饰函数的方式。这与副作用或状态无关。这是关于构图的。正如我们稍后将看到的,修饰函数可用于表达各种效果或状态,但这不是 monad 的用途。单子是一种粘性管道胶带,将修饰函数的一端连接到修饰函数的另一端。
This kind of a definition also expresses what a monad really is: it’s a way of composing embellished functions. It’s not about side effects or state. It’s about composition. As we’ll see later, embellished functions may be used to express a variety of effects or state, but that’s not what the monad is for. The monad is the sticky duct tape that ties one end of an embellished function to the other end of an embellished function.
回到我们的Writer示例:日志记录函数(函Writer子的 Kleisli 箭头)形成一个范畴,因为Writer是一个 monad:
Going back to our Writer example: The logging functions (the Kleisli arrows for the Writer functor) form a category because Writer is a monad:
instance Monoid w => Monad (Writer w) where
f >=> g = \a ->
let Writer (b, s) = f a
Writer (c, s') = g b
in Writer (c, s `mappend` s')
return a = Writer (a, mempty)instance Monoid w => Monad (Writer w) where
f >=> g = \a ->
let Writer (b, s) = f a
Writer (c, s') = g b
in Writer (c, s `mappend` s')
return a = Writer (a, mempty)
Writer w只要满足幺半群法则w,就满足单子法则(它们也不能在 Haskell 中强制执行)。
Monad laws for Writer w are satisfied as long as monoid laws for w are satisfied (they can’t be enforced in Haskell either).
Writer为monad定义了一个有用的 Kleisli 箭头,称为tell。它的唯一目的是将其参数添加到日志中:
There’s a useful Kleisli arrow defined for the Writer monad called tell. It’s sole purpose is to add its argument to the log:
tell :: w -> Writer w ()
tell s = Writer ((), s)tell :: w -> Writer w ()
tell s = Writer ((), s)
稍后我们将使用它作为其他一元函数的构建块。
We’ll use it later as a building block for other monadic functions.
当为不同的 monad 实现 Fish 操作符时,您很快就会意识到大量代码是重复的,并且可以很容易地分解出来。首先,两个函数的 Kleisli 组合必须返回一个函数,因此它的实现也可以从采用 类型参数的 lambda 开始a:
When implementing the fish operator for different monads you quickly realize that a lot of code is repeated and can be easily factored out. To begin with, the Kleisli composition of two functions must return a function, so its implementation may as well start with a lambda taking an argument of type a:
(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c)
f >=> g = \a -> ...(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c)
f >=> g = \a -> ...
我们对此参数唯一能做的就是将其传递给f:
The only thing we can do with this argument is to pass it to f:
f >=> g = \a -> let mb = f a
in ...f >=> g = \a -> let mb = f a
in ...
此时,我们必须生成 type 的结果m c,并拥有可供我们使用的 type 对象m b和函数g :: b -> m c。让我们定义一个函数来为我们做这件事。这个函数称为bind,通常以中缀运算符的形式编写:
At this point we have to produce the result of type m c, having at our disposal an object of type m b and a function g :: b -> m c. Let’s define a function that does that for us. This function is called bind and is usually written in the form of an infix operator:
(>>=) :: m a -> (a -> m b) -> m b(>>=) :: m a -> (a -> m b) -> m b
对于每个 monad,我们可以定义绑定,而不是定义 Fish 操作符。事实上,monad 的标准 Haskell 定义使用了 bind:
For every monad, instead of defining the fish operator, we may instead define bind. In fact the standard Haskell definition of a monad uses bind:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m aclass Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
这是 monad 的绑定定义Writer:
Here’s the definition of bind for the Writer monad:
(Writer (a, w)) >>= f = let Writer (b, w') = f a
in Writer (b, w `mappend` w')(Writer (a, w)) >>= f = let Writer (b, w') = f a
in Writer (b, w `mappend` w')
确实比鱼经营者的定义要短。
It is indeed shorter than the definition of the fish operator.
利用函子这一事实,可以进一步剖析绑定m。我们可以使用fmap将该函数应用于a -> m b的内容m a。这将a变成m b. 因此,应用程序的结果类型为m (m b)。这并不完全是我们想要的——我们需要类型的结果m b——但我们已经很接近了。我们所需要的只是一个能够折叠或展平 的双重应用的函数m。这样的函数称为join:
It’s possible to further dissect bind, taking advantage of the fact that m is a functor. We can use fmap to apply the function a -> m b to the contents of m a. This will turn a into m b. The result of the application is therefore of type m (m b). This is not exactly what we want — we need the result of type m b — but we’re close. All we need is a function that collapses or flattens the double application of m. Such function is called join:
join :: m (m a) -> m ajoin :: m (m a) -> m a
使用join,我们可以将绑定重写为:
Using join, we can rewrite bind as:
ma >>= f = join (fmap f ma)ma >>= f = join (fmap f ma)
这引出了定义 monad 的第三个选项:
That leads us to the third option for defining a monad:
class Functor m => Monad m where
join :: m (m a) -> m a
return :: a -> m aclass Functor m => Monad m where
join :: m (m a) -> m a
return :: a -> m a
在这里我们明确要求是m一个Functor. 在 monad 的前两个定义中我们不必这样做。这是因为任何m支持 Fish 或 Bind 运算符的类型构造函数都自动成为函子。例如,可以fmap根据 bind 和进行定义return:
Here we have explicitly requested that m be a Functor. We didn’t have to do that in the previous two definitions of the monad. That’s because any type constructor m that either supports the fish or bind operator is automatically a functor. For instance, it’s possible to define fmap in terms of bind and return:
fmap f ma = ma >>= \a -> return (f a)fmap f ma = ma >>= \a -> return (f a)
为了完整起见,这里join是Writermonad:
For completeness, here’s join for the Writer monad:
join :: Monoid w => Writer w (Writer w a) -> Writer w a
join (Writer ((Writer (a, w')), w)) = Writer (a, w `mappend` w')join :: Monoid w => Writer w (Writer w a) -> Writer w a
join (Writer ((Writer (a, w')), w)) = Writer (a, w `mappend` w')
do_do Notation使用 monad 编写代码的一种方法是使用 Kleisli 箭头——使用 Fish 运算符来编写它们。这种编程模式是无点风格的推广。无点代码非常紧凑并且通常非常优雅。但总的来说,它可能很难理解,近乎神秘。这就是为什么大多数程序员更喜欢为函数参数和中间值命名。
One way of writing code using monads is to work with Kleisli arrows — composing them using the fish operator. This mode of programming is the generalization of the point-free style. Point-free code is compact and often quite elegant. In general, though, it can be hard to understand, bordering on cryptic. That’s why most programmers prefer to give names to function arguments and intermediate values.
当处理单子时,这意味着绑定操作符比鱼操作符更受青睐。Bind 接受一个单子值并返回一个单子值。程序员可以选择为这些值命名。但这并不是什么进步。我们真正想要的是假装我们正在处理常规值,而不是封装它们的单子容器。这就是命令式代码的工作原理——副作用(例如更新全局日志)大部分都隐藏在视图之外。这就是doHaskell 中的符号模拟的内容。
When dealing with monads it means favoring the bind operator over the fish operator. Bind takes a monadic value and returns a monadic value. The programmer may chose to give names to those values. But that’s hardly an improvement. What we really want is to pretend that we are dealing with regular values, not the monadic containers that encapsulate them. That’s how imperative code works — side effects, such as updating a global log, are mostly hidden from view. And that’s what the do notation emulates in Haskell.
那么您可能想知道,为什么要使用 monad?如果我们想让副作用不可见,为什么不坚持使用命令式语言呢?答案是 monad 使我们能够更好地控制副作用。例如,monad 中的日志Writer从一个函数传递到另一个函数,并且永远不会全局公开。不可能出现日志混乱或造成数据争用的情况。此外,单子代码与程序的其余部分有明确的划分和隔离。
You might be wondering then, why use monads at all? If we want to make side effects invisible, why not stick to an imperative language? The answer is that the monad gives us much better control over side effects. For instance, the log in the Writer monad is passed from function to function and is never exposed globally. There is no possibility of garbling the log or creating a data race. Also, monadic code is clearly demarcated and cordoned off from the rest of the program.
该do符号只是一元组合的语法糖。从表面上看,它看起来很像命令式代码,但它直接转换为一系列绑定和 lambda 表达式。
The do notation is just syntactic sugar for monadic composition. On the surface, it looks a lot like imperative code, but it translates directly to a sequence of binds and lambda expressions.
例如,以我们之前用来说明 monad 中 Kleisli 箭头的组成的示例为例Writer。使用我们当前的定义,它可以重写为:
For instance, take the example we used previously to illustrate the composition of Kleisli arrows in the Writer monad. Using our current definitions, it could be rewritten as:
process :: String -> Writer String [String]
process = upCase >=> toWordsprocess :: String -> Writer String [String]
process = upCase >=> toWords
该函数将输入字符串中的所有字符转换为大写并将其拆分为单词,同时生成其操作的日志。
This function turns all characters in the input string to upper case and splits it into words, all the while producing a log of its actions.
在do符号中它看起来像这样:
In the do notation it would look like this:
process s = do
upStr <- upCase s
toWords upStrprocess s = do
upStr <- upCase s
toWords upStr
这里,upStr只是一个String,即使upCase产生一个Writer:
Here, upStr is just a String, even though upCase produces a Writer:
upCase :: String -> Writer String String
upCase s = Writer (map toUpper s, "upCase ")upCase :: String -> Writer String String
upCase s = Writer (map toUpper s, "upCase ")
这是因为do编译器对该块进行了脱糖处理,以便:
This is because the do block is desugared by the compiler to:
process s =
upCase s >>= \ upStr ->
toWords upStrprocess s =
upCase s >>= \ upStr ->
toWords upStr
的一元结果upCase绑定到采用 的 lambda String。这是显示在do块中的该字符串的名称。阅读该行时:
The monadic result of upCase is bound to a lambda that takes a String. It’s the name of this string that shows up in the do block. When reading the line:
upStr <- upCase supStr <- upCase s
我们说upStr 得到的结果是upCase s。
we say that upStr gets the result of upCase s.
当我们内联时,伪命令风格更加明显toWords。我们将其替换为对 的调用tell,该调用会记录字符串"toWords ",然后调用 来获取使用return分割字符串的结果。请注意,这是一个处理字符串的常规函数。upStrwordswords
The pseudo-imperative style is even more pronounced when we inline toWords. We replace it with the call to tell, which logs the string "toWords ", followed by the call to return with the result of splitting the string upStr using words. Notice that words is a regular function working on strings.
process s = do
upStr <- upStr s
tell "toWords "
return (words upStr)process s = do
upStr <- upStr s
tell "toWords "
return (words upStr)
这里,do 块中的每一行在脱糖代码中引入了一个新的嵌套绑定:
Here, each line in the do block introduces a new nested bind in the desugared code:
process s =
upCase s >>= \upStr ->
tell "toWords " >>= \() ->
return (words upStr)process s =
upCase s >>= \upStr ->
tell "toWords " >>= \() ->
return (words upStr)
请注意,它tell会生成一个单位值,因此不必将其传递给后面的 lambda。忽略一元结果的内容(但不是它的效果 - 这里是对日志的贡献)是很常见的,因此在这种情况下有一个特殊的运算符来替换绑定:
Notice that tell produces a unit value, so it doesn’t have to be passed to the following lambda. Ignoring the contents of a monadic result (but not its effect — here, the contribution to the log) is quite common, so there is a special operator to replace bind in that case:
(>>) :: m a -> m b -> m b
m >> k = m >>= (\_ -> k)(>>) :: m a -> m b -> m b
m >> k = m >>= (\_ -> k)
我们的代码的实际脱糖如下所示:
The actual desugaring of our code looks like this:
process s =
upCase s >>= \upStr ->
tell "toWords " >>
return (words upStr)process s =
upCase s >>= \upStr ->
tell "toWords " >>
return (words upStr)
一般来说,do块由行(或子块)组成,它们要么使用向左箭头引入新名称,然后在代码的其余部分中可用,要么纯粹为了副作用而执行。绑定运算符在代码行之间是隐式的。顺便说一句,在 Haskell 中,可以用do大括号和分号替换块中的格式。这为将 monad 描述为重载分号的方式提供了理由。
In general, do blocks consist of lines (or sub-blocks) that either use the left arrow to introduce new names that are then available in the rest of the code, or are executed purely for side-effects. Bind operators are implicit between the lines of code. Incidentally, it is possible, in Haskell, to replace the formatting in the do blocks with braces and semicolons. This provides the justification for describing the monad as a way of overloading the semicolon.
请注意,在对符号进行脱糖处理时,嵌套 lambda 和绑定运算符do会do根据每行的结果影响块其余部分的执行。该属性可用于引入复杂的控制结构,例如模拟异常。
Notice that the nesting of lambdas and bind operators when desugaring the do notation has the effect of influencing the execution of the rest of the do block based on the result of each line. This property can be used to introduce complex control structures, for instance to simulate exceptions.
有趣的是,该符号的等价物do已在命令式语言(尤其是 C++)中得到应用。我说的是可恢复函数或协程。C++ future 形成 monad已经不是什么秘密了。这是延续单子的一个例子,我们很快就会讨论它。延续的问题是它们很难组合。在 Haskell 中,我们使用do符号将“我的处理程序将调用你的处理程序”的意大利面条变成看起来非常像顺序代码的东西。可恢复函数使 C++ 中的相同转换成为可能。并且可以应用相同的机制将意大利面条状的嵌套循环转换为列表推导式或“生成器”,这本质上是do列表单子的表示法。如果没有统一的 monad 抽象,这些问题通常都是通过为语言提供自定义扩展来解决的。在 Haskell 中,这一切都是通过库来处理的。
Interestingly, the equivalent of the do notation has found its application in imperative languages, C++ in particular. I’m talking about resumable functions or coroutines. It’s not a secret that C++ futures form a monad. It’s an example of the continuation monad, which we’ll discuss shortly. The problem with continuations is that they are very hard to compose. In Haskell, we use the do notation to turn the spaghetti of “my handler will call your handler” into something that looks very much like sequential code. Resumable functions make the same transformation possible in C++. And the same mechanism can be applied to turn the spaghetti of nested loops into list comprehensions or “generators,” which are essentially the do notation for the list monad. Without the unifying abstraction of the monad, each of these problems is typically addressed by providing custom extensions to the language. In Haskell, this is all dealt with through libraries.
现在我们知道了 monad 的用途——它让我们可以编写修饰函数——真正有趣的问题是为什么修饰函数在函数式编程中如此重要。我们已经看到了一个例子,Writermonad,其中的修饰让我们可以跨多个函数调用创建和累积日志。本来可以使用非纯函数(例如,通过访问和修改某些全局状态)解决的问题现在可以使用纯函数解决。
Now that we know what the monad is for — it lets us compose embellished functions — the really interesting question is why embellished functions are so important in functional programming. We’ve already seen one example, the Writer monad, where embellishment let us create and accumulate a log across multiple function calls. A problem that would otherwise be solved using impure functions (e.g., by accessing and modifying some global state) was solved with pure functions.
以下是从Eugenio Moggi 的开创性论文中复制的类似问题的简短列表,所有这些问题传统上都是通过放弃函数的纯粹性来解决的。
Here is a short list of similar problems, copied from Eugenio Moggi’s seminal paper, all of which are traditionally solved by abandoning the purity of functions.
真正令人兴奋的是,所有这些问题都可以使用相同的巧妙技巧来解决:转向修饰函数。当然,每种情况下的装饰都会完全不同。
What really is mind blowing is that all these problems may be solved using the same clever trick: turning to embellished functions. Of course, the embellishment will be totally different in each case.
你必须意识到,在这个阶段,并不要求修饰是一元的。只有当我们坚持组合时——能够将单个修饰函数分解为更小的修饰函数——我们才需要一个 monad。同样,由于每个装饰不同,单子组合的实现方式也会不同,但总体模式是相同的。这是一个非常简单的模式:具有结合性且具有同一性的组合。
You have to realize that, at this stage, there is no requirement that the embellishment be monadic. It’s only when we insist on composition — being able to decompose a single embellished function into smaller embellished functions — that we need a monad. Again, since each of the embellishments is different, monadic composition will be implemented differently, but the overall pattern is the same. It’s a very simple pattern: composition that is associative and equipped with identity.
下一节主要介绍 Haskell 示例。如果您渴望回到范畴论或者您已经熟悉 Haskell 的 monad 实现,请随意浏览甚至跳过它。
The next section is heavy on Haskell examples. Feel free to skim or even skip it if you’re eager to get back to category theory or if you’re already familiar with Haskell’s implementation of monads.
首先,我们来分析一下 monad 的使用方式Writer。我们从一个执行特定任务的纯函数开始——给定参数,它产生特定的输出。我们用另一个函数替换了这个函数,该函数通过将原始输出与字符串配对来修饰原始输出。这就是我们解决日志记录问题的方法。
First, let’s analyze the way we used the Writer monad. We started with a pure function that performed a certain task — given arguments, it produced a certain output. We replaced this function with another function that embellished the original output by pairing it with a string. That was our solution to the logging problem.
我们不能就此止步,因为一般来说,我们不想处理单一的解决方案。我们需要能够将一个日志生成函数分解为更小的日志生成函数。正是这些较小函数的组合让我们有了 monad 的概念。
We couldn’t stop there because, in general, we don’t want to deal with monolithic solutions. We needed to be able to decompose one log-producing function into smaller log-producing functions. It’s the composition of those smaller functions that led us to the concept of a monad.
真正令人惊奇的是,修饰函数返回类型的相同模式适用于通常需要放弃纯度的各种问题。让我们浏览一下我们的列表并依次找出适用于每个问题的修饰。
What’s really amazing is that the same pattern of embellishing the function return types works for a large variety of problems that normally would require abandoning purity. Let’s go through our list and identify the embellishment that applies to each problem in turn.
我们修改每个可能不会终止的函数的返回类型,将其转换为“提升”类型——一种包含原始类型的所有值加上特殊“底部”值的类型⊥。例如,Bool类型作为一个集合,将包含两个元素:True和False。提升Bool包含三个要素。返回提升值的函数Bool可能会产生True或False,或 永远执行。
We modify the return type of every function that may not terminate by turning it into a “lifted” type — a type that contains all values of the original type plus the special “bottom” value ⊥. For instance, the Bool type, as a set, would contain two elements: True and False. The lifted Bool contains three elements. Functions that return the lifted Bool may produce True or False, or execute forever.
有趣的是,在像 Haskell 这样的惰性语言中,一个永无止境的函数实际上可能会返回一个值,并且这个值可能会传递给下一个函数。我们将这个特殊值称为底部。只要该值不是明确需要的(例如,要进行模式匹配或作为输出生成),就可以传递它而不会停止程序的执行。因为每个 Haskell 函数都可能是非终止的,所以 Haskell 中的所有类型都被假定为被提升。这就是为什么我们经常谈论 Haskell(提升)类型和函数的范畴Hask而不是更简单的Set。不过,尚不清楚Hask是否是一个真正的范畴(请参阅这篇Andrej Bauer 帖子)。
The funny thing is that, in a lazy language like Haskell, a never-ending function may actually return a value, and this value may be passed to the next function. We call this special value the bottom. As long as this value is not explicitly needed (for instance, to be pattern matched, or produced as output), it may be passed around without stalling the execution of the program. Because every Haskell function may be potentially non-terminating, all types in Haskell are assumed to be lifted. This is why we often talk about the category Hask of Haskell (lifted) types and functions rather than the simpler Set. It is not clear, though, that Hask is a real category (see this Andrej Bauer post).
如果一个函数可以返回许多不同的结果,那么它最好一次返回所有结果。从语义上讲,非确定性函数相当于返回结果列表的函数。这对于惰性垃圾收集语言来说很有意义。例如,如果您只需要一个值,则可以只获取列表的头部,而尾部将永远不会被评估。如果需要随机值,请使用随机数生成器来选择列表的第 n 个元素。惰性甚至允许您返回无限的结果列表。
If a function can return many different results, it may as well return them all at once. Semantically, a non-deterministic function is equivalent to a function that returns a list of results. This makes a lot of sense in a lazy garbage-collected language. For instance, if all you need is one value, you can just take the head of the list, and the tail will never be evaluated. If you need a random value, use a random number generator to pick the n-th element of the list. Laziness even allows you to return an infinite list of results.
在列表中,monad(Haskell 的非确定性计算实现)join被实现为concat. 请记住,这join应该展平容器的容器 -concat将列表的列表连接成单个列表。return创建一个单例列表:
In the list monad — Haskell’s implementation of nondeterministic computations — join is implemented as concat. Remember that join is supposed to flatten a container of containers — concat concatenates a list of lists into a single list. return creates a singleton list:
instance Monad [] where
join = concat
return x = [x]instance Monad [] where
join = concat
return x = [x]
列表 monad 的绑定运算符由以下通用公式给出:fmap后跟join其,在本例中给出:
The bind operator for the list monad is given by the general formula: fmap followed by join which, in this case gives:
as >>= k = concat (fmap k as)as >>= k = concat (fmap k as)
在这里,函数k本身生成一个列表,并应用于列表中的每个元素as。结果是一个列表列表,使用 进行展平concat。
Here, the function k, which itself produces a list, is applied to every element of the list as. The result is a list of lists, which is flattened using concat.
从程序员的角度来看,使用列表比在循环中调用非确定性函数或实现返回迭代器的函数更容易(尽管在现代 C++ 中,返回惰性范围几乎是相当于在 Haskell 中返回一个列表)。
From the programmer’s point of view, working with a list is easier than, for instance, calling a non-deterministic function in a loop, or implementing a function that returns an iterator (although, in modern C++, returning a lazy range would be almost equivalent to returning a list in Haskell).
创造性地使用非确定性的一个很好的例子是游戏编程。例如,当计算机与人类下国际象棋时,它无法预测对手的下一步行动。然而,它可以生成所有可能的动作的列表,并一一分析它们。类似地,非确定性解析器可以生成给定表达式的所有可能解析的列表。
A good example of using non-determinism creatively is in game programming. For instance, when a computer plays chess against a human, it can’t predict the opponent’s next move. It can, however, generate a list of all possible moves and analyze them one by one. Similarly, a non-deterministic parser may generate a list of all possible parses for a given expression.
尽管我们可能将返回列表的函数解释为不确定的,但列表 monad 的应用要广泛得多。这是因为将生成列表的计算拼接在一起是命令式编程中使用的迭代结构(循环)的完美功能替代品。通常可以使用fmap将循环体应用于列表的每个元素来重写单个循环。列表 monad 中的符号do可用于替换复杂的嵌套循环。
Even though we may interpret functions returning lists as non-deterministic, the applications of the list monad are much wider. That’s because stitching together computations that produce lists is a perfect functional substitute for iterative constructs — loops — that are used in imperative programming. A single loop can be often rewritten using fmap that applies the body of the loop to each element of the list. The do notation in the list monad can be used to replace complex nested loops.
我最喜欢的例子是生成毕达哥拉斯三元组的程序——可以形成直角三角形边的正整数三元组。
My favorite example is the program that generates Pythagorean triples — triples of positive integers that can form sides of right triangles.
triples = do
z <- [1..]
x <- [1..z]
y <- [x..z]
guard (x^2 + y^2 == z^2)
return (x, y, z)triples = do
z <- [1..]
x <- [1..z]
y <- [x..z]
guard (x^2 + y^2 == z^2)
return (x, y, z)
第一行告诉我们z从无限正数列表中获取一个元素[1..]。然后从 1 到 之间的(有限)数字x列表中获取一个元素。最后从和之间的数字列表中获取一个元素。我们有三个号码可供使用。该函数接受一个表达式并返回一个单位列表:[1..z]zyxz1 <= x <= y <= zguardBool
The first line tells us that z gets an element from an infinite list of positive numbers [1..]. Then x gets an element from the (finite) list [1..z] of numbers between 1 and z. Finally y gets an element from the list of numbers between x and z. We have three numbers 1 <= x <= y <= z at our disposal. The function guard takes a Bool expression and returns a list of units:
guard :: Bool -> [()]
guard True = [()]
guard False = []guard :: Bool -> [()]
guard True = [()]
guard False = []
这个函数(它是一个更大的类的成员,称为MonadPlus)在这里用于过滤掉非毕达哥拉斯三元组。>>事实上,如果您查看 bind (或相关运算符)的实现,您会注意到,当给定一个空列表时,它会生成一个空列表。另一方面,当给定一个非空列表(此处为包含 unit 的单例列表[()])时,bind 将调用延续,此处return (x, y, z),它会生成一个带有经过验证的毕达哥拉斯三元组的单例列表。所有这些单例列表将通过封闭的绑定连接起来以产生最终(无限)结果。当然,调用者triples永远无法消耗整个列表,但这并不重要,因为 Haskell 很懒。
This function (which is a member of a larger class called MonadPlus) is used here to filter out non-Pythagorean triples. Indeed, if you look at the implementation of bind (or the related operator >>), you’ll notice that, when given an empty list, it produces an empty list. On the other hand, when given a non-empty list (here, the singleton list containing unit [()]), bind will call the continuation, here return (x, y, z), which produces a singleton list with a verified Pythagorean triple. All those singleton lists will be concatenated by the enclosing binds to produce the final (infinite) result. Of course, the caller of triples will never be able to consume the whole list, but that doesn’t matter, because Haskell is lazy.
在列表 monad 和符号的帮助下,通常需要一组三个嵌套循环的问题已得到极大简化do。好像这还不够,Haskell 让您使用列表理解进一步简化此代码:
The problem that normally would require a set of three nested loops has been dramatically simplified with the help of the list monad and the do notation. As if that weren’t enough, Haskell let’s you simplify this code even further using list comprehension:
triples = [(x, y, z) | z <- [1..]
, x <- [1..z]
, y <- [x..z]
, x^2 + y^2 == z^2]triples = [(x, y, z) | z <- [1..]
, x <- [1..z]
, y <- [x..z]
, x^2 + y^2 == z^2]
这只是列表单子的进一步语法糖(严格来说,MonadPlus)。
This is just further syntactic sugar for the list monad (strictly speaking, MonadPlus).
您可能会在其他函数式或命令式语言中以生成器和协程的形式看到类似的构造。
You might see similar constructs in other functional or imperative languages under the guise of generators and coroutines.
对某些外部状态或环境具有只读访问权限的函数始终可以替换为将该环境作为附加参数的函数。纯函数(a, e) -> b(其中e是环境类型)乍一看并不像克莱斯利箭头。但一旦我们将其柯里化,a -> (e -> b)我们就会认识到这个修饰是我们的老朋友读者函子:
A function that has read-only access to some external state, or environment, can be always replaced by a function that takes that environment as an additional argument. A pure function (a, e) -> b (where e is the type of the environment) doesn’t look, at first sight, like a Kleisli arrow. But as soon as we curry it to a -> (e -> b) we recognize the embellishment as our old friend the reader functor:
newtype Reader e a = Reader (e -> a)newtype Reader e a = Reader (e -> a)
您可以将返回 a 的函数解释Reader为生成迷你可执行文件:给定环境的操作会产生所需的结果。有一个辅助函数runReader可以执行这样的操作:
You may interpret a function returning a Reader as producing a mini-executable: an action that given an environment produces the desired result. There is a helper function runReader to execute such an action:
runReader :: Reader e a -> e -> a
runReader (Reader f) e = f erunReader :: Reader e a -> e -> a
runReader (Reader f) e = f e
对于不同的环境值可能会产生不同的结果。
It may produce different results for different values of the environment.
请注意,返回 a 的函数Reader和Reader操作本身都是纯的。
Notice that both the function returning a Reader, and the Reader action itself are pure.
要实现 monad 的绑定Reader,首先请注意,您必须生成一个函数,该函数接受环境e并生成b:
To implement bind for the Reader monad, first notice that you have to produce a function that takes the environment e and produces a b:
ra >>= k = Reader (\e -> ...)ra >>= k = Reader (\e -> ...)
在 lambda 内部,我们可以执行操作ra来生成a:
Inside the lambda, we can execute the action ra to produce an a:
ra >>= k = Reader (\e -> let a = runReader ra e
in ...)ra >>= k = Reader (\e -> let a = runReader ra e
in ...)
然后我们可以将 传递a给延续k以获取新操作rb:
We can then pass the a to the continuation k to get a new action rb:
ra >>= k = Reader (\e -> let a = runReader ra e
rb = k a
in ...)ra >>= k = Reader (\e -> let a = runReader ra e
rb = k a
in ...)
rb最后,我们可以在环境中运行操作e:
Finally, we can run the action rb with the environment e:
ra >>= k = Reader (\e -> let a = runReader ra e
rb = k a
in runReader rb e)ra >>= k = Reader (\e -> let a = runReader ra e
rb = k a
in runReader rb e)
为了实现,return我们创建了一个忽略环境并返回未更改值的操作。
To implement return we create an action that ignores the environment and returns the unchanged value.
将它们放在一起,经过一些简化后,我们得到以下定义:
Putting it all together, after a few simplifications, we get the following definition:
instance Monad (Reader e) where
ra >>= k = Reader (\e -> runReader (k (runReader ra e)) e)
return x = Reader (\e -> x)instance Monad (Reader e) where
ra >>= k = Reader (\e -> runReader (k (runReader ra e)) e)
return x = Reader (\e -> x)
这只是我们最初的日志示例。修饰由Writer函子给出:
This is just our initial logging example. The embellishment is given by the Writer functor:
newtype Writer w a = Writer (a, w)newtype Writer w a = Writer (a, w)
为了完整起见,还有一个简单的帮助程序runWriter可以解压数据构造函数:
For completeness, there’s also a trivial helper runWriter that unpacks the data constructor:
runWriter :: Writer w a -> (a, w)
runWriter (Writer (a, w)) = (a, w)runWriter :: Writer w a -> (a, w)
runWriter (Writer (a, w)) = (a, w)
正如我们之前所看到的,为了可Writer组合,w必须是一个幺半群。Writer这是用绑定运算符编写的monad 实例:
As we’ve seen before, in order to make Writer composable, w has to be a monoid. Here’s the monad instance for Writer written in terms of the bind operator:
instance (Monoid w) => Monad (Writer w) where
(Writer (a, w)) >>= k = let (a', w') = runWriter (k a)
in Writer (a', w `mappend` w')
return a = Writer (a, mempty)instance (Monoid w) => Monad (Writer w) where
(Writer (a, w)) >>= k = let (a', w') = runWriter (k a)
in Writer (a', w `mappend` w')
return a = Writer (a, mempty)
对状态具有读/写访问权限的函数结合了Reader和 的修饰Writer。您可以将它们视为纯函数,将状态作为额外参数并生成一对值/状态作为结果:(a, s) -> (b, s)。柯里化后,我们将它们变成 Kleisli arrows 的形式a -> (s -> (b, s)),并在函子中抽象出修饰State:
Functions that have read/write access to state combine the embellishments of the Reader and the Writer. You may think of them as pure functions that take the state as an extra argument and produce a pair value/state as a result: (a, s) -> (b, s). After currying, we get them into the form of Kleisli arrows a -> (s -> (b, s)), with the embellishment abstracted in the State functor:
newtype State s a = State (s -> (a, s))newtype State s a = State (s -> (a, s))
同样,我们可以将 Kleisli 箭头视为返回一个操作,可以使用辅助函数执行该操作:
Again, we can look at a Kleisli arrow as returning an action, which can be executed using the helper function:
runState :: State s a -> s -> (a, s)
runState (State f) s = f srunState :: State s a -> s -> (a, s)
runState (State f) s = f s
不同的初始状态不仅可能产生不同的结果,而且最终状态也会不同。
Different initial states may not only produce different results, but also different final states.
monad的 bind 实现State与 monad 的实现非常相似Reader,除了必须注意在每一步传递正确的状态:
The implementation of bind for the State monad is very similar to that of the Reader monad, except that care has to be taken to pass the correct state at each step:
sa >>= k = State (\s -> let (a, s') = runState sa s
sb = k a
in runState sb s')sa >>= k = State (\s -> let (a, s') = runState sa s
sb = k a
in runState sb s')
这是完整的实例:
Here’s the full instance:
instance Monad (State s) where
sa >>= k = State (\s -> let (a, s') = runState sa s
in runState (k a) s')
return a = State (\s -> (a, s))instance Monad (State s) where
sa >>= k = State (\s -> let (a, s') = runState sa s
in runState (k a) s')
return a = State (\s -> (a, s))
还有两个辅助 Kleisli 箭头可用于操纵状态。其中之一检索状态进行检查:
There are also two helper Kleisli arrows that may be used to manipulate the state. One of them retrieves the state for inspection:
get :: State s s
get = State (\s -> (s, s))get :: State s s
get = State (\s -> (s, s))
另一个用全新的状态替换它:
and the other replaces it with a completely new state:
put :: s -> State s ()
put s' = State (\s -> ((), s'))put :: s -> State s ()
put s' = State (\s -> ((), s'))
抛出异常的命令式函数实际上是一个部分函数——它是一个没有为其参数的某些值定义的函数。就纯总函数而言,异常的最简单实现使用Maybe函子。部分函数被扩展为总函数,Just a无论何时有意义或Nothing不有意义,它都会返回。如果我们还想返回一些有关失败原因的信息,我们可以使用Either仿函数(第一个类型固定,例如 to String)。
An imperative function that throws an exception is really a partial function — it’s a function that’s not defined for some values of its arguments. The simplest implementation of exceptions in terms of pure total functions uses the Maybe functor. A partial function is extended to a total function that returns Just a whenever it makes sense, and Nothing when it doesn’t. If we want to also return some information about the cause of the failure, we can use the Either functor instead (with the first type fixed, for instance, to String).
这是Monad的实例Maybe:
Here’s the Monad instance for Maybe:
instance Monad Maybe where
Nothing >>= k = Nothing
Just a >>= k = k a
return a = Just ainstance Monad Maybe where
Nothing >>= k = Nothing
Just a >>= k = k a
return a = Just a
请注意,当检测到错误时,正确的一元组合会使Maybe计算短路(k永远不会调用延续)。这就是我们期望的异常行为。
Notice that monadic composition for Maybe correctly short-circuits the computation (the continuation k is never called) when an error is detected. That’s the behavior we expect from exceptions.
这就是“不要打电话给我们,我们会打电话给你!” 面试后您可能会遇到的情况。您应该提供一个处理程序,一个使用结果调用的函数,而不是获得直接答案。当调用时未知结果时,这种编程风格特别有用,因为例如,它正在由另一个线程评估或从远程网站传递。在这种情况下,Kleisli 箭头返回一个接受处理程序的函数,该处理程序表示“计算的其余部分”:
It’s the “Don’t call us, we’ll call you!” situation you may experience after a job interview. Instead of getting a direct answer, you are supposed to provide a handler, a function to be called with the result. This style of programming is especially useful when the result is not known at the time of the call because, for instance, it’s being evaluated by another thread or delivered from a remote web site. A Kleisli arrow in this case returns a function that accepts a handler, which represents “the rest of the computation”:
data Cont r a = Cont ((a -> r) -> r)data Cont r a = Cont ((a -> r) -> r)
当最终调用handlera -> r时,它会生成 type 的结果r,并在最后返回该结果。延续由结果类型参数化。(实际上,这通常是某种状态指示器。)
The handler a -> r, when it’s eventually called, produces the result of type r, and this result is returned at the end. A continuation is parameterized by the result type. (In practice, this is often some kind of status indicator.)
还有一个辅助函数用于执行 Kleisli 箭头返回的操作。它接受处理程序并将其传递给延续:
There is also a helper function for executing the action returned by the Kleisli arrow. It takes the handler and passes it to the continuation:
runCont :: Cont r a -> (a -> r) -> r
runCont (Cont k) h = k hrunCont :: Cont r a -> (a -> r) -> r
runCont (Cont k) h = k h
连续的组合是出了名的困难,因此通过单子(特别是符号do)对其进行处理具有极大的优势。
The composition of continuations is notoriously difficult, so its handling through a monad and, in particular, the do notation, is of extreme advantage.
我们来看看bind的实现。首先让我们看一下精简后的签名:
Let’s figure out the implementation of bind. First let’s look at the stripped down signature:
(>>=) :: ((a -> r) -> r) ->
(a -> (b -> r) -> r) ->
((b -> r) -> r)(>>=) :: ((a -> r) -> r) ->
(a -> (b -> r) -> r) ->
((b -> r) -> r)
我们的目标是创建一个接受处理程序(b -> r)并生成结果的函数r。这就是我们的出发点:
Our goal is to create a function that takes the handler (b -> r) and produces the result r. So that’s our starting point:
ka >>= kab = Cont (\hb -> ...)ka >>= kab = Cont (\hb -> ...)
在 lambda 内部,我们希望ka使用代表其余计算的适当处理程序来调用该函数。我们将这个处理程序实现为 lambda:
Inside the lambda, we want to call the function ka with the appropriate handler that represents the rest of the computation. We’ll implement this handler as a lambda:
runCont ka (\a -> ...)runCont ka (\a -> ...)
在这种情况下,其余的计算涉及首先调用kabwith a,然后传递hb给结果操作kb:
In this case, the rest of the computation involves first calling kab with a, and then passing hb to the resulting action kb:
runCont ka (\a -> let kb = kab a
in runCont kb hb)runCont ka (\a -> let kb = kab a
in runCont kb hb)
正如您所看到的,延续是由内而外组成的。最终的处理程序hb是从计算的最内层调用的。这是完整的实例:
As you can see, continuations are composed inside out. The final handler hb is called from the innermost layer of the computation. Here’s the full instance:
instance Monad (Cont r) where
ka >>= kab = Cont (\hb -> runCont ka (\a -> runCont (kab a) hb))
return a = Cont (\ha -> ha a)instance Monad (Cont r) where
ka >>= kab = Cont (\hb -> runCont ka (\a -> runCont (kab a) hb))
return a = Cont (\ha -> ha a)
这是最棘手的问题,也是许多混乱的根源。显然,像 之类的函数getChar如果要返回在键盘上键入的字符,则不可能是纯粹的。但是如果它返回容器内的字符怎么办?只要无法从该容器中提取字符,我们就可以声称该函数是纯函数。每次调用getChar它都会返回完全相同的容器。从概念上讲,该容器将包含所有可能字符的叠加。
This is the trickiest problem and a source of a lot of confusion. Clearly, a function like getChar, if it were to return a character typed at the keyboard, couldn’t be pure. But what if it returned the character inside a container? As long as there was no way of extracting the character from this container, we could claim that the function is pure. Every time you call getChar it would return exactly the same container. Conceptually, this container would contain the superposition of all possible characters.
如果您熟悉量子力学,那么理解这个类比应该没有问题。它就像里面装着薛定谔的猫的盒子一样——只不过无法打开或窥视盒子内部。该盒子是使用特殊的内置IO函子定义的。在我们的示例中,getChar可以声明为 Kleisli 箭头:
If you’re familiar with quantum mechanics, you should have no problem understanding this analogy. It’s just like the box with the Schrödinger’s cat inside — except that there is no way to open or peek inside the box. The box is defined using the special built-in IO functor. In our example, getChar could be declared as a Kleisli arrow:
getChar :: () -> IO ChargetChar :: () -> IO Char
(实际上,由于单位类型的函数相当于选择返回类型的值,因此 的声明getChar被简化为getChar :: IO Char。)
(Actually, since a function from the unit type is equivalent to picking a value of the return type, the declaration of getChar is simplified to getChar :: IO Char.)
作为函子,IO您可以使用 来操纵其内容fmap。而且,作为函子,它可以存储任何类型的内容,而不仅仅是字符。IO当你考虑到 Haskell 中的monad时,这种方法的真正实用性就会显现出来。这意味着您能够组合产生IO对象的 Kleisli 箭头。
Being a functor, IO lets you manipulate its contents using fmap. And, as a functor, it can store the contents of any type, not just a character. The real utility of this approach comes to light when you consider that, in Haskell, IO is a monad. It means that you are able to compose Kleisli arrows that produce IO objects.
你可能认为克莱斯利组合可以让你窥视物体的内容IO(如果我们继续量子类比,从而“波函数崩溃”)。事实上,您可以getChar使用另一个 Kleisli 箭头来组合,该箭头接受一个字符,并将其转换为整数。问题是第二个 Kleisli 箭头只能以(IO Int). 同样,您最终将得到所有可能整数的叠加。等等。薛定谔的猫永远不会泄露秘密。一旦你进入了IO单子,就没有办法摆脱它了。没有与单子相对应runState的runReader东西IO。没有runIO!
You might think that Kleisli composition would allow you to peek at the contents of the IO object (thus “collapsing the wave function,” if we were to continue the quantum analogy). Indeed, you could compose getChar with another Kleisli arrow that takes a character and, say, converts it to an integer. The catch is that this second Kleisli arrow could only return this integer as an (IO Int). Again, you’ll end up with a superposition of all possible integers. And so on. The Schrödinger’s cat is never out of the bag. Once you are inside the IO monad, there is no way out of it. There is no equivalent of runState or runReader for the IO monad. There is no runIO!
IO那么,除了与另一个 Kleisli 箭头组合之外,您还能对 Kleisli 箭头的结果(对象)做什么呢?嗯,你可以从 退货main。在 Haskell 中,main有签名:
So what can you do with the result of a Kleisli arrow, the IO object, other than compose it with another Kleisli arrow? Well, you can return it from main. In Haskell, main has the signature:
main :: IO ()main :: IO ()
你可以随意将其视为 Kleisli 箭头:
and you are free to think of it as a Kleisli arrow:
main :: () -> IO ()main :: () -> IO ()
从这个角度来看,Haskell 程序只是 monad 中的一根 Kleisli 大箭头IO。您可以使用单子组合将较小的 Kleisli 箭头组合成它。由运行时系统对结果IO对象执行某些操作(也称为IO操作)。
From that perspective, a Haskell program is just one big Kleisli arrow in the IO monad. You can compose it from smaller Kleisli arrows using monadic composition. It’s up to the runtime system to do something with the resulting IO object (also called IO action).
请注意,箭头本身是一个纯函数——它一直向下都是纯函数。肮脏的工作被归咎于系统。当它最终执行IO从 返回的操作main时,它会执行各种令人讨厌的事情,例如读取用户输入、修改文件、打印令人讨厌的消息、格式化磁盘等等。Haskell 程序从不弄脏自己的手(好吧,除了它调用 时unsafePerformIO,但那是一个不同的故事)。
Notice that the arrow itself is a pure function — it’s pure functions all the way down. The dirty work is relegated to the system. When it finally executes the IO action returned from main, it does all kinds of nasty things like reading user input, modifying files, printing obnoxious messages, formatting a disk, and so on. The Haskell program never dirties its hands (well, except when it calls unsafePerformIO, but that’s a different story).
当然,因为 Haskell 很懒,main几乎立即返回,肮脏的工作立即开始。在执行操作的过程中,会IO根据需要请求和评估纯计算的结果。因此,实际上,程序的执行是纯(Haskell)代码和脏(系统)代码的交错。
Of course, because Haskell is lazy, main returns almost immediately, and the dirty work begins right away. It’s during the execution of the IO action that the results of pure computations are requested and evaluated on demand. So, in reality, the execution of a program is an interleaving of pure (Haskell) and dirty (system) code.
对单子还有另一种解释IO,这种解释更加奇怪,但作为数学模型却非常有意义。它将整个宇宙视为程序中的一个对象。请注意,从概念上讲,命令式模型将 Universe 视为外部全局对象,因此执行 I/O 的过程会因与该对象交互而产生副作用。他们都可以读取和修改宇宙的状态。
There is an alternative interpretation of the IO monad that is even more bizarre but makes perfect sense as a mathematical model. It treats the whole Universe as an object in a program. Notice that, conceptually, the imperative model treats the Universe as an external global object, so procedures that perform I/O have side effects by virtue of interacting with that object. They can both read and modify the state of the Universe.
我们已经知道如何在函数式编程中处理状态——我们使用状态 monad。然而,与简单状态不同的是,宇宙的状态无法使用标准数据结构轻松描述。但我们不必这样做,只要我们不直接与它互动即可。我们假设存在一种类型就足够了RealWorld,并且通过宇宙工程的某种奇迹,运行时能够提供这种类型的对象。动作IO只是一个函数:
We already know how to deal with state in functional programming — we use the state monad. Unlike simple state, however, the state of the Universe cannot be easily described using standard data structures. But we don’t have to, as long as we never directly interact with it. It’s enough that we assume that there exists a type RealWorld and, by some miracle of cosmic engineering, the runtime is able to provide an object of this type. An IO action is just a function:
type IO a = RealWorld -> (a, RealWorld)type IO a = RealWorld -> (a, RealWorld)
或者,就单子而言State:
Or, in terms of the State monad:
type IO = State RealWorldtype IO = State RealWorld
然而,>=>单子return必须IO内置到语言中。
However, >=> and return for the IO monad have to be built into the language.
相同的IOmonad 用于封装交互式输出。RealWorld应该包含所有输出设备。你可能想知道为什么我们不能直接调用 Haskell 的输出函数并假装它们什么都不做。例如,为什么我们有:
The same IO monad is used to encapsulate interactive output. RealWorld is supposed to contain all output devices. You might wonder why we can’t just call output functions from Haskell and pretend that they do nothing. For instance, why do we have:
putStr :: String -> IO ()putStr :: String -> IO ()
而不是更简单的:
rather than the simpler:
putStr :: String -> ()putStr :: String -> ()
原因有两个:Haskell 很懒,因此它永远不会调用其输出(此处为单位对象)不用于任何用途的函数。而且,即使它不是懒惰的,它仍然可以随意更改此类调用的顺序,从而使输出变得混乱。在 Haskell 中强制顺序执行两个函数的唯一方法是通过数据依赖。一个函数的输入必须依赖于另一个函数的输出。RealWorld在动作之间传递会IO强制排序。
Two reasons: Haskell is lazy, so it would never call a function whose output — here, the unit object — is not used for anything. And, even if it weren’t lazy, it could still freely change the order of such calls and thus garble the output. The only way to force sequential execution of two functions in Haskell is through data dependency. The input of one function must depend on the output of another. Having RealWorld passed between IO actions enforces sequencing.
从概念上讲,在这个程序中:
Conceptually, in this program:
main :: IO ()
main = do
putStr "Hello "
putStr "World!"main :: IO ()
main = do
putStr "Hello "
putStr "World!"
打印“World!”的动作 接收“Hello”已在屏幕上的宇宙作为输入。它输出一个新的宇宙,带有“Hello World!” 屏幕上。
the action that prints “World!” receives, as input, the Universe in which “Hello ” is already on the screen. It outputs a new Universe, with “Hello World!” on the screen.
当然,我只是触及了单子编程的皮毛。Monad 不仅使用纯函数完成命令式编程中通常会产生副作用的任务,而且还具有高度的控制和类型安全性。但它们并非没有缺点。关于 monad 的主要抱怨是它们不容易相互组合。当然,您可以使用 monad 转换器库组合大多数基本 monad。创建一个将状态与异常相结合的 monad 堆栈相对容易,但没有将任意 monad 堆栈在一起的公式。
Of course I have just scratched the surface of monadic programming. Monads not only accomplish, with pure functions, what normally is done with side effects in imperative programming, but they also do it with a high degree of control and type safety. They are not without drawbacks, though. The major complaint about monads is that they don’t easily compose with each other. Granted, you can combine most of the basic monads using the monad transformer library. It’s relatively easy to create a monad stack that combines, say, state with exceptions, but there is no formula for stacking arbitrary monads together.
如果你向程序员提到单子,你可能最终会谈论效果。对于数学家来说,单子是关于代数的。我们稍后会讨论代数——它们在编程中发挥着重要作用——但首先我想让你对它们与单子的关系有一些直观的了解。就目前而言,这有点摇摆不定,但请耐心听我说。
If you mention monads to a programmer, you’ll probably end up talking about effects. To a mathematician, monads are about algebras. We’ll talk about algebras later — they play an important role in programming — but first I’d like to give you a little intuition about their relation to monads. For now, it’s a bit of a hand-waving argument, but bear with me.
代数是关于创建、操作和评估表达式的。表达式是使用运算符构建的。考虑这个简单的表达式:
Algebra is about creating, manipulating, and evaluating expressions. Expressions are built using operators. Consider this simple expression:
x2 + 2 x + 1x2 + 2 x + 1
x该表达式由、 等变量和 1 或 2 等常量与 plus 或 times 等运算符绑定在一起形成。作为程序员,我们经常将表达式视为树。
This expression is formed using variables like x, and constants like 1 or 2, bound together with operators like plus or times. As programmers, we often think of expressions as trees.
树是容器,因此,更一般地说,表达式是用于存储变量的容器。在范畴论中,我们将容器表示为内函子。如果我们将类型分配a给变量x,我们的表达式将具有类型m a,其中m是构建表达式树的endofunctor。(非平凡的分支表达式通常是使用递归定义的endofunctor创建的。)
Trees are containers so, more generally, an expression is a container for storing variables. In category theory, we represent containers as endofunctors. If we assign the type a to the variable x, our expression will have the type m a, where m is an endofunctor that builds expression trees. (Nontrivial branching expressions are usually created using recursively defined endofunctors.)
可以对表达式执行的最常见操作是什么?它是替换:用表达式替换变量。例如,在我们的示例中,我们可以替换x为y - 1以获得:
What’s the most common operation that can be performed on an expression? It’s substitution: replacing variables with expressions. For instance, in our example, we could replace x with y - 1 to get:
(y - 1)2 + 2 (y - 1) + 1(y - 1)2 + 2 (y - 1) + 1
发生的事情是这样的:我们采用类型表达式m a并应用类型转换a -> m b(b表示 的类型y)。结果是类型为 的表达式m b。让我拼写一下:
Here’s what happened: We took an expression of type m a and applied a transformation of type a -> m b (b represents the type of y). The result is an expression of type m b. Let me spell it out:
m a -> (a -> m b) -> m bm a -> (a -> m b) -> m b
是的,这就是一元绑定的签名。
Yes, that’s the signature of monadic bind.
那是一点动力。现在让我们来了解一下 monad 的数学原理。数学家使用与程序员不同的符号。他们更喜欢使用字母T表示endofunctor,以及希腊字母:μ 代表join, η 代表return。和join都是return多态函数,因此我们可以猜测它们对应于自然变换。
That was a bit of motivation. Now let’s get to the math of the monad. Mathematicians use different notation than programmers. They prefer to use the letter T for the endofunctor, and Greek letters: μ for join and η for return. Both join and return are polymorphic functions, so we can guess that they correspond to natural transformations.
因此,在范畴论中,单子被定义为T配备一对自然变换 μ 和 η 的内函子。
Therefore, in category theory, a monad is defined as an endofunctor T equipped with a pair of natural transformations μ and η.
μ 是从函子的平方T2回到 的自然变换T。平方只是与自身组成的函子T ∘ T(我们只能对内函子进行这种平方)。
μ is a natural transformation from the square of the functor T2 back to T. The square is simply the functor composed with itself, T ∘ T (we can only do this kind of squaring for endofunctors).
μ :: T2 -> Tμ :: T2 -> T
对象的这种自然变换的组成部分a是态射:
The component of this natural transformation at an object a is the morphism:
μa :: T (T a) -> T aμa :: T (T a) -> T a
在Hask中,它直接转换为我们对 的定义join。
which, in Hask, translates directly to our definition of join.
Iη 是恒等函子和之间的自然变换T:
η is a natural transformation between the identity functor I and T:
η :: I -> Tη :: I -> T
I考虑到对物体的作用a只是a, η 的分量由态射给出:
Considering that the action of I on the object a is just a, the component of η is given by the morphism:
ηa :: a -> T aηa :: a -> T a
这直接转化为我们对 的定义return。
which translates directly to our definition of return.
这些自然变换必须满足一些额外的定律。看待它的一种方式是,这些定律让我们为内函子定义一个克莱斯利范畴T。a请记住,和之间的克莱斯利箭头b被定义为态射a -> T b。两个这样的箭头的组合(我将其写为带有下标 的圆圈T)可以使用 μ 来实现:
These natural transformations must satisfy some additional laws. One way of looking at it is that these laws let us define a Kleisli category for the endofunctor T. Remember that a Kleisli arrow between a and b is defined as a morphism a -> T b. The composition of two such arrows (I’ll write it as a circle with the subscript T) can be implemented using μ:
g ∘T f = μc ∘ (T g) ∘ fg ∘T f = μc ∘ (T g) ∘ f
在哪里
where
f :: a -> T b
g :: b -> T cf :: a -> T b
g :: b -> T c
这里T,作为函子,可以应用于态射g。用 Haskell 表示法可能更容易识别这个公式:
Here T, being a functor, can be applied to the morphism g. It might be easier to recognize this formula in Haskell notation:
f >=> g = join . fmap g . ff >=> g = join . fmap g . f
或者,在组件中:
or, in components:
(f >=> g) a = join (fmap g (f a))(f >=> g) a = join (fmap g (f a))
就代数解释而言,我们只是组合两个连续的替换。
In terms of the algebraic interpretation, we are just composing two successive substitutions.
为了使克莱斯利箭头形成一个范畴,我们希望它们的组合是关联的,并且 η a是 处的恒等式克莱斯利箭头a。这个要求可以转化为 μ 和 η 的一元定律。但还有另一种推导这些定律的方法,使它们看起来更像幺半群定律。事实上μ常被称为乘法、η单位。
For Kleisli arrows to form a category we want their composition to be associative, and ηa to be the identity Kleisli arrow at a. This requirement can be translated to monadic laws for μ and η. But there is another way of deriving these laws that makes them look more like monoid laws. In fact μ is often called multiplication, and η unit.
T粗略地说,结合律规定,将、T3、的立方减少到 的两种方法T必须给出相同的结果。两个单位定律(左和右)表明,当 被η应用到T,然后减去 时μ,我们得到T。
Roughly speaking, the associativity law states that the two ways of reducing the cube of T, T3, down to T must give the same result. Two unit laws (left and right) state that when η is applied to T and then reduced by μ, we get back T.
事情有点棘手,因为我们正在编写自然变换和函子。因此,有必要回顾一下水平构图。例如,可以看作是afterT3的组合。我们可以将两个自然变换的水平合成应用于它:TT2
Things are a little tricky because we are composing natural transformations and functors. So a little refresher on horizontal composition is in order. For instance, T3 can be seen as a composition of T after T2. We can apply to it the horizontal composition of two natural transformations:
IT ∘ μIT ∘ μ
并得到T∘T;T通过应用可以进一步减少到μ. 是从到 的IT身份自然变换。您经常会看到这种水平构图的符号缩写为。这种表示法是明确的,因为用自然变换组成函子是没有意义的,因此必须在这种情况下表示。TTIT ∘ μT∘μTIT
and get T∘T; which can be further reduced to T by applying μ. IT is the identity natural transformation from T to T. You will often see the notation for this type of horizontal composition IT ∘ μ shortened to T∘μ. This notation is unambiguous because it makes no sense to compose a functor with a natural transformation, therefore T must mean IT in this context.
我们还可以在(内)函子范畴中绘制图表[C, C]:
We can also draw the diagram in the (endo-) functor category [C, C]:
或者,我们可以将T3其视为 的组合T2∘T并应用于μ∘T它。结果也T∘T可以再次简化为T使用 μ。我们要求两条路径产生相同的结果。
Alternatively, we can treat T3 as the composition of T2∘T and apply μ∘T to it. The result is also T∘T which, again, can be reduced to T using μ. We require that the two paths produce the same result.
类似地,我们可以将水平组合应用于得到后的η∘T恒等函子的组合,然后可以使用 来约简。结果应该与我们直接将恒等自然变换应用于 相同。并且,以此类推,对于 也应该如此。ITT2μTT∘η
Similarly, we can apply the horizontal composition η∘T to the composition of the identity functor I after T to obtain T2, which can then be reduced using μ. The result should be the same as if we applied the identity natural transformation directly to T. And, by analogy, the same should be true for T∘η.
您可以说服自己,这些定律保证克莱斯利箭头的组成确实满足范畴定律。
You can convince yourself that these laws guarantee that the composition of Kleisli arrows indeed satisfies the laws of a category.
单子和幺半群之间的相似之处是惊人的。我们有乘法 μ、单位 η、结合律和单位定律。但是我们对幺半群的定义太狭窄,无法将单子描述为幺半群。那么让我们概括一下幺半群的概念。
The similarities between a monad and a monoid are striking. We have multiplication μ, unit η, associativity, and unit laws. But our definition of a monoid is too narrow to describe a monad as a monoid. So let’s generalize the notion of a monoid.
让我们回到幺半群的传统定义。它是一个包含二元运算和一个称为单位的特殊元素的集合。在 Haskell 中,这可以表示为类型类:
Let’s go back to the conventional definition of a monoid. It’s a set with a binary operation and a special element called unit. In Haskell, this can be expressed as a typeclass:
class Monoid m where
mappend :: m -> m -> m
mempty :: mclass Monoid m where
mappend :: m -> m -> m
mempty :: m
二元运算mappend必须是结合且单位的(即乘以单位mempty是无操作)。
The binary operation mappend must be associative and unital (i.e., multiplication by the unit mempty is a no-op).
请注意,在 Haskell 中, 的定义mappend是柯里化的。它可以解释为将 的每个元素映射m到一个函数:
Notice that, in Haskell, the definition of mappend is curried. It can be interpreted as mapping every element of m to a function:
mappend :: m -> (m -> m)mappend :: m -> (m -> m)
正是这种解释产生了将幺半群定义为单一对象范畴,其中自同态(m -> m)代表幺半群的元素。但是因为 Haskell 内置了柯里化,我们也可以从不同的乘法定义开始:
It’s this interpretation that gives rise to the definition of a monoid as a single-object category where endomorphisms (m -> m) represent the elements of the monoid. But because currying is built into Haskell, we could as well have started with a different definition of multiplication:
mu :: (m, m) -> mmu :: (m, m) -> m
在这里,笛卡尔积(m, m)成为要相乘的对的来源。
Here, the cartesian product (m, m) becomes the source of pairs to be multiplied.
这个定义提出了一条不同的泛化路径:用分类积代替笛卡尔积。我们可以从全局定义产品的范畴开始,m在那里选择一个对象,并将乘法定义为态射:
This definition suggests a different path to generalization: replacing the cartesian product with categorical product. We could start with a category where products are globally defined, pick an object m there, and define multiplication as a morphism:
μ :: m × m -> mμ :: m × m -> m
但我们有一个问题:在任意范畴中,我们无法窥视对象的内部,那么我们如何选择单位元素呢?这是有一个技巧的。还记得元素选择如何等同于单例集中的函数吗?mempty在 Haskell 中,我们可以用函数替换 的定义:
We have one problem though: In an arbitrary category we can’t peek inside an object, so how do we pick the unit element? There is a trick to it. Remember how element selection is equivalent to a function from the singleton set? In Haskell, we could replace the definition of mempty with a function:
eta :: () -> meta :: () -> m
单例是Set中的终端对象,因此很自然地将此定义推广到任何具有终端对象的范畴t:
The singleton is the terminal object in Set, so it’s natural to generalize this definition to any category that has a terminal object t:
η :: t -> mη :: t -> m
这让我们可以选择单位“元素”,而不必谈论元素。
This lets us pick the unit “element” without having to talk about elements.
与我们之前将幺半群定义为单一对象范畴不同,这里的幺半群定律不会自动满足——我们必须强加它们。但为了表述它们,我们必须建立基础分类乘积本身的幺半群结构。首先让我们回想一下幺半群结构在 Haskell 中是如何工作的。
Unlike in our previous definition of a monoid as a single-object category, monoidal laws here are not automatically satisfied — we have to impose them. But in order to formulate them we have to establish the monoidal structure of the underlying categorical product itself. Let’s recall how monoidal structure works in Haskell first.
我们从结合性开始。在 Haskell 中,相应的方程组为:
We start with associativity. In Haskell, the corresponding equational law is:
mu x (mu y z) = mu (mu x y) zmu x (mu y z) = mu (mu x y) z
在我们将其推广到其他范畴之前,我们必须将其重写为函数等式(态射)。我们必须将其从其对各个变量的作用中抽象出来——换句话说,我们必须使用无点符号。知道笛卡尔积是一个双函子,我们可以将左侧写为:
Before we can generalize it to other categories, we have to rewrite it as an equality of functions (morphisms). We have to abstract it away from its action on individual variables — in other words, we have to use point-free notation. Knowning that the cartesian product is a bifunctor, we can write the left hand side as:
(mu . bimap id mu)(x, (y, z))(mu . bimap id mu)(x, (y, z))
右侧为:
and the right hand side as:
(mu . bimap mu id)((x, y), z)(mu . bimap mu id)((x, y), z)
这几乎就是我们想要的。不幸的是,笛卡尔积不是严格关联的——(x, (y, z))不一样((x, y), z)——所以我们不能只写无点:
This is almost what we want. Unfortunately, the cartesian product is not strictly associative — (x, (y, z)) is not the same as ((x, y), z) — so we can’t just write point-free:
mu . bimap id mu = mu . bimap mu idmu . bimap id mu = mu . bimap mu id
另一方面,pairs 的两个嵌套是同构的。有一个称为关联器的可逆函数可以在它们之间进行转换:
On the other hand, the two nestings of pairs are isomorphic. There is an invertible function called the associator that converts between them:
alpha :: ((a, b), c) -> (a, (b, c))
alpha ((x, y), z) = (x, (y, z))alpha :: ((a, b), c) -> (a, (b, c))
alpha ((x, y), z) = (x, (y, z))
在关联器的帮助下,我们可以写出 的无点结合律mu:
With the help of the associator, we can write the point-free associativity law for mu:
mu . bimap id mu . alpha = mu . bimap mu idmu . bimap id mu . alpha = mu . bimap mu id
我们可以将类似的技巧应用于单位定律,在新的表示法中,其形式为:
We can apply a similar trick to unit laws which, in the new notation, take the form:
mu (eta (), x) = x
mu (x, eta ()) = xmu (eta (), x) = x
mu (x, eta ()) = x
它们可以重写为:
They can be rewritten as:
(mu . bimap eta id) ((), x) = lambda ((), x)
(mu . bimap id eta) (x, ()) = rho (x, ())(mu . bimap eta id) ((), x) = lambda ((), x)
(mu . bimap id eta) (x, ()) = rho (x, ())
同构lambda和rho分别称为左单位子和右单位子。他们见证了这样一个事实:该单位()是同构的笛卡尔积的恒等式:
The isomorphisms lambda and rho are called the left and right unitor, respectively. They witness the fact that the unit () is the identity of the cartesian product up to isomorphism:
lambda :: ((), a) -> a
lambda ((), x) = xlambda :: ((), a) -> a
lambda ((), x) = x
rho :: (a, ()) -> a
rho (x, ()) = xrho :: (a, ()) -> a
rho (x, ()) = x
因此,单位定律的无点版本是:
The point-free versions of the unit laws are therefore:
mu . bimap id eta = lambda
mu . bimap eta id = rhomu . bimap id eta = lambda
mu . bimap eta id = rho
mu我们利用以下事实制定了无点幺半群定律eta:底层笛卡尔积本身就像类型范畴中的幺群乘法。但请记住,笛卡尔积的结合律和单位定律仅在同构时有效。
We have formulated point-free monoidal laws for mu and eta using the fact that the underlying cartesian product itself acts like a monoidal multiplication in the category of types. Keep in mind though that the associativity and unit laws for the cartesian product are valid only up to isomorphism.
事实证明,这些定律可以推广到任何具有产品和终端对象的范畴。范畴积确实在同构上是关联的,而终端对象是单元,也在同构上是关联的。缔合子和两个单位子是自然同构。这些定律可以用通勤图来表示。
It turns out that these laws can be generalized to any category with products and a terminal object. Categorical products are indeed associative up to isomorphism and the terminal object is the unit, also up to isomorphism. The associator and the two unitors are natural isomorphisms. The laws can be represented by commuting diagrams.
请注意,因为该乘积是一个双函子,所以它可以提升一对态射——在 Haskell 中,这是使用 完成的bimap。
Notice that, because the product is a bifunctor, it can lift a pair of morphisms — in Haskell this was done using bimap.
我们可以在这里停下来,说我们可以在任何具有分类产品和终端对象的范畴之上定义一个幺半群。只要我们能选出一个对象m和两个满足幺半群定律的态射 μ 和 η,我们就有了一个幺半群。但我们可以做得更好。我们不需要成熟的分类乘积来制定 μ 和 η 的定律。回想一下,产品是通过使用投影的通用结构来定义的。我们在制定幺半群定律时没有使用任何投影。
We could stop here and say that we can define a monoid on top of any category with categorical products and a terminal object. As long as we can pick an object m and two morphisms μ and η that satisfy monoidal laws, we have a monoid. But we can do better than that. We don’t need a full-blown categorical product to formulate the laws for μ and η. Recall that a product is defined through a universal construction that uses projections. We haven’t used any projections in our formulation of monoidal laws.
行为类似于乘积而不是乘积的双函子称为张量乘积,通常用中缀运算符 ⊗ 表示。一般来说,张量积的定义有点棘手,但我们不会担心它。我们只列出它的属性——最重要的是结合性到同构性。
A bifunctor that behaves like a product without being a product is called a tensor product, often denoted by the infix operator ⊗. A definition of a tensor product in general is a bit tricky, but we won’t worry about it. We’ll just list its properties — the most important being associativity up to isomorphism.
同样,我们不需要该对象t是终端。我们从未使用过它的终结属性——即任何对象到它的唯一态射的存在。我们要求的是它与张量积能够很好地配合。这意味着我们希望它成为张量积的单位,再次达到同构。让我们把它们放在一起:
Similarly, we don’t need the object t to be terminal. We never used its terminal property — namely, the existence of a unique morphism from any object to it. What we require is that it works well in concert with the tensor product. Which means that we want it to be the unit of the tensor product, again, up to isomorphism. Let’s put it all together:
幺半群范畴是配备有称为张量积的双函子的C类:
A monoidal category is a category C equipped with a bifunctor called the tensor product:
⊗ :: C × C -> C⊗ :: C × C -> C
以及一个称为单位对象的不同对象i,以及三个自然同构,分别称为关联器和左右单位器:
and a distinct object i called the unit object, together with three natural isomorphisms called, respectively, the associator and the left and right unitors:
αa b c :: (a ⊗ b) ⊗ c -> a ⊗ (b ⊗ c)
λa :: i ⊗ a -> a
ρa :: a ⊗ i -> aαa b c :: (a ⊗ b) ⊗ c -> a ⊗ (b ⊗ c)
λa :: i ⊗ a -> a
ρa :: a ⊗ i -> a
(还有一个用于简化四重张量积的相干条件。)
(There is also a coherence condition for simplifying a quadruple tensor product.)
重要的是张量积描述了许多熟悉的双函子。特别是,它适用于乘积、余乘积,以及我们很快就会看到的,适用于内函子的组合(也适用于一些更深奥的乘积,如日卷积)。幺半群范畴将在丰富范畴的制定中发挥重要作用。
What’s important is that a tensor product describes many familiar bifunctors. In particular, it works for a product, a coproduct and, as we’ll see shortly, for the composition of endofunctors (and also for some more esoteric products like Day convolution). Monoidal categories will play an essential role in the formulation of enriched categories.
我们现在准备在更一般的幺半群范畴设置中定义幺半群。我们首先选择一个对象m。使用张量积我们可以形成 的幂m。的平方m是m ⊗ m。形成 的立方有两种方法m,但它们通过关联器是同构的。类似地,对于 的更高幂m(这就是我们需要相干条件的地方)。为了形成幺半群,我们需要选择两个态射:
We are now ready to define a monoid in a more general setting of a monoidal category. We start by picking an object m. Using the tensor product we can form powers of m. The square of m is m ⊗ m. There are two ways of forming the cube of m, but they are isomorphic through the associator. Similarly for higher powers of m (that’s where we need the coherence conditions). To form a monoid we need to pick two morphisms:
μ :: m ⊗ m -> m
η :: i -> mμ :: m ⊗ m -> m
η :: i -> m
其中i是张量积的单位对象。
where i is the unit object for our tensor product.
这些态射必须满足结合律和单位律,可以用以下交换图来表示:
These morphisms have to satisfy associativity and unit laws, which can be expressed in terms of the following commuting diagrams:
请注意,张量积必须是双函子,因为我们需要提升态射对来形成诸如μ ⊗ id或 之类的积η ⊗ id。这些图表只是我们之前分类产品结果的简单概括。
Notice that it’s essential that the tensor product be a bifunctor because we need to lift pairs of morphisms to form products such as μ ⊗ id or η ⊗ id. These diagrams are just a straightforward generalization of our previous results for categorical products.
幺半群结构会出现在意想不到的地方。其中一个地方就是函子范畴。如果你稍微眯一下眼睛,你可能会发现函子组合是乘法的一种形式。问题在于,任何两个函子都不能组合——其中一个函子的目标范畴必须是另一个函子的源范畴。这只是态射复合的通常规则——而且,正如我们所知,函子确实是Cat范畴中的态射。但就像自同态(循环回到同一个对象的态射)总是可组合的一样,内函子也是如此。对于任何给定的范畴C,从C到C 的内函子形成函子范畴[C, C]。它的对象是内函子,态射是它们之间的自然变换。我们可以从这个范畴中取出任意两个对象,例如 endofunctorsF和G,并生成第三个对象F ∘ G- 一个 endofunctors,它是它们的组合。
Monoidal structures pop up in unexpected places. One such place is the functor category. If you squint a little, you might be able to see functor composition as a form of multiplication. The problem is that not any two functors can be composed — the target category of one has to be the source category of the other. That’s just the usual rule of composition of morphisms — and, as we know, functors are indeed morphisms in the category Cat. But just like endomorphisms (morphisms that loop back to the same object) are always composable, so are endofunctors. For any given category C, endofunctors from C to C form the functor category [C, C]. Its objects are endofunctors, and morphisms are natural transformations between them. We can take any two objects from this category, say endofunctors F and G, and produce a third object F ∘ G — an endofunctor that’s their composition.
内函子组合是张量积的良好候选者吗?首先,我们必须确定它是一个双函子。它可以用来提升一对态射——这里是自然变换吗?张量积的类似物的签名bimap如下所示:
Is endofunctor composition a good candidate for a tensor product? First, we have to establish that it’s a bifunctor. Can it be used to lift a pair of morphisms — here, natural transformations? The signature of the analog of bimap for the tensor product would look something like this:
bimap :: (a -> b) -> (c -> d) -> (a ⊗ c -> b ⊗ d)bimap :: (a -> b) -> (c -> d) -> (a ⊗ c -> b ⊗ d)
如果用内函子替换对象,用自然变换替换箭头,用组合替换张量积,您将得到:
If you replace objects by endofunctors, arrows by natural transformations, and tensor products by composition, you get:
(F -> F') -> (G -> G') -> (F ∘ G -> F' ∘ G')(F -> F') -> (G -> G') -> (F ∘ G -> F' ∘ G')
您可能会认为这是水平构图的特例。
which you may recognize as the special case of horizontal composition.
我们还可以使用恒等函子I,它可以作为恒函器组合的恒等式——我们的新张量积。此外,函子组合是结合的。事实上,结合律和单位律是严格的——不需要结合子或两个单位律。因此,内函子形成了一个严格的幺半群范畴,其中函子复合作为张量积。
We also have at our disposal the identity endofunctor I, which can serve as the identity for endofunctor composition — our new tensor product. Moreover, functor composition is associative. In fact associativity and unit laws are strict — there’s no need for the associator or the two unitors. So endofunctors form a strict monoidal category with functor composition as tensor product.
这个范畴中的幺半群是什么?它是一个对象——即一个endofunctor T;和两个态射——这是自然变换:
What’s a monoid in this category? It’s an object — that is an endofunctor T; and two morphisms — that is natural transformations:
μ :: T ∘ T -> T
η :: I -> Tμ :: T ∘ T -> T
η :: I -> T
不仅如此,这里还有幺半群定律:
Not only that, here are the monoid laws:
它们正是我们之前见过的单子定律。现在你明白了桑德斯·麦克莱恩的名言:
They are exactly the monad laws we’ve seen before. Now you understand the famous quote from Saunders Mac Lane:
总而言之,monad 只是内函子范畴中的一个幺半群。
All told, monad is just a monoid in the category of endofunctors.
您可能在函数式编程会议上看到过它印在一些 T 恤上。
You might have seen it emblazoned on some t-shirts at functional programming conferences.
附加词,是一对在两个范畴C和DL ⊣ R之间来回移动的函子。有两种组合它们的方法,产生两个内函子和。根据附加,这些内函子通过称为单位和数量的两种自然变换与恒等函子相关:R ∘ LL ∘ R
An adjunction, L ⊣ R, is a pair of functors going back and forth between two categories C and D. There are two ways of composing them giving rise to two endofunctors, R ∘ L and L ∘ R. As per an adjunction, these endofunctors are related to identity functors through two natural transformations called unit and counit:
η :: ID -> R ∘ L
ε :: L ∘ R -> ICη :: ID -> R ∘ L
ε :: L ∘ R -> IC
我们立即看到附加词的单位看起来就像单子的单位一样。事实证明,endofunctorR ∘ L确实是一个 monad。我们所需要做的就是定义适当的 μ 来配合 η。这是我们的内函子的平方和内函子本身之间的自然变换,或者就伴随函子而言:
Immediately we see that the unit of an adjunction looks just like the unit of a monad. It turns out that the endofunctor R ∘ L is indeed a monad. All we need is to define the appropriate μ to go with the η. That’s a natural transformation between the square of our endofunctor and the endofunctor itself or, in terms of the adjoint functors:
R ∘ L ∘ R ∘ L -> R ∘ LR ∘ L ∘ R ∘ L -> R ∘ L
事实上,我们可以使用计数来折叠L ∘ R中间的。μ 的精确公式由水平组合给出:
And, indeed, we can use the counit to collapse the L ∘ R in the middle. The exact formula for μ is given by the horizontal composition:
μ = R ∘ ε ∘ Lμ = R ∘ ε ∘ L
一元律源自附加律和交换律的单位和单位所满足的恒等式。
Monadic laws follow from the identities satisfied by the unit and counit of the adjunction and the interchange law.
在 Haskell 中,我们没有看到很多从附加词派生出来的单子,因为附加词通常涉及两个范畴。然而,指数或函数对象的定义是一个例外。这是形成这个附加的两个endofunctor:
We don’t see a lot of monads derived from adjunctions in Haskell, because an adjunction usually involves two categories. However, the definitions of an exponential, or a function object, is an exception. Here are the two endofunctors that form this adjunction:
L z = z × s
R b = s ⇒ bL z = z × s
R b = s ⇒ b
您可能会将它们的组成视为熟悉的状态单子:
You may recognize their composition as the familiar state monad:
R (L z) = s ⇒ (z × s)R (L z) = s ⇒ (z × s)
我们之前在 Haskell 中见过这个 monad:
We’ve seen this monad before in Haskell:
newtype State s a = State (s -> (a, s))newtype State s a = State (s -> (a, s))
我们还可以将附加语翻译成 Haskell。左边的函子是乘积函子:
Let’s also translate the adjunction to Haskell. The left functor is the product functor:
newtype Prod s a = Prod (a, s)newtype Prod s a = Prod (a, s)
正确的函子是 reader 函子:
and the right functor is the reader functor:
newtype Reader s a = Reader (s -> a)newtype Reader s a = Reader (s -> a)
它们构成附属语:
They form the adjunction:
instance Adjunction (Prod s) (Reader s) where
counit (Prod (Reader f, s)) = f s
unit a = Reader (\s -> Prod (a, s))instance Adjunction (Prod s) (Reader s) where
counit (Prod (Reader f, s)) = f s
unit a = Reader (\s -> Prod (a, s))
您可以轻松地说服自己,积函子之后的读者函子的组成确实相当于状态函子:
You can easily convince yourself that the composition of the reader functor after the product functor is indeed equivalent to the state functor:
newtype State s a = State (s -> (a, s))newtype State s a = State (s -> (a, s))
正如预期的那样,unit附加的 相当于return状态单子的功能。通过评估作用counit于其参数的函数来进行操作。这可以被识别为该函数的未柯里化版本runState:
As expected, the unit of the adjunction is equivalent to the return function of the state monad. The counit acts by evaluating a function acting on its argument. This is recognizable as the uncurried version of the function runState:
runState :: State s a -> s -> (a, s)
runState (State f) s = f srunState :: State s a -> s -> (a, s)
runState (State f) s = f s
(未柯里化,因为counit它作用于一对)。
(uncurried, because in counit it acts on a pair).
我们现在可以将join状态单子定义为自然变换 μ 的一个组成部分。为此,我们需要三个自然变换的水平组合:
We can now define join for the state monad as a component of the natural transformation μ. For that we need a horizontal composition of three natural transformations:
μ = R ∘ ε ∘ Lμ = R ∘ ε ∘ L
换句话说,我们需要将计数 ε 偷偷地穿过读者函子的一层。我们不能fmap直接调用,因为编译器会选择函子State,而不是Reader函子。但请记住,fmap对于读者来说,函子只是留下了函数组合。所以我们直接使用函数组合。
In other words, we need to sneak the counit ε across one level of the reader functor. We can’t just call fmap directly, because the compiler would pick the one for the State functor, rather than the Reader functor. But recall that fmap for the reader functor is just left function composition. So we’ll use function composition directly.
我们必须首先剥离数据构造函数State以暴露函子内的函数State。这是使用以下方法完成的runState:
We have to first peel off the data constructor State to expose the function inside the State functor. This is done using runState:
ssa :: State s (State s a)
runState ssa :: s -> (State s a, s)ssa :: State s (State s a)
runState ssa :: s -> (State s a, s)
然后我们将其与由 定义的 count 进行左组合uncurry runState。最后,我们将它放回State数据构造函数中:
Then we left-compose it with the counit, which is defined by uncurry runState. Finally, we clothe it back in the State data constructor:
join :: State s (State s a) -> State s a
join ssa = State (uncurry runState . runState ssa)join :: State s (State s a) -> State s a
join ssa = State (uncurry runState . runState ssa)
join这确实是monad的实现State。
This is indeed the implementation of join for the State monad.
事实证明,不仅每个附加词都会产生一个单子,反之亦然:每个单子都可以分解为两个伴随函子的组合。但这种因式分解并不是唯一的。
It turns out that not only every adjunction gives rise to a monad, but the converse is also true: every monad can be factorized into a composition of two adjoint functors. Such factorization is not unique though.
L ∘ R我们将在下一节中讨论另一个 endofunctor 。
We’ll talk about the other endofunctor L ∘ R in the next section.
现在我们已经涵盖了 monad,我们可以通过反转箭头并在相反的范畴中工作来获得二元性的好处并免费获得 comonad。
Now that we have covered monads, we can reap the benefits of duality and get comonads for free simply by reversing the arrows and working in the opposite category.
回想一下,在最基本的层面上,单子是关于组成 Kleisli 箭头:
Recall that, at the most basic level, monads are about composing Kleisli arrows:
a -> m ba -> m b
其中m是一个函子,它是一个单子。如果我们使用字母w(upside down m) 表示 comonad,我们可以将 co-Kleisli 箭头定义为该类型的态射:
where m is a functor that is a monad. If we use the letter w (upside down m) for the comonad, we can define co-Kleisli arrows as morphism of the type:
w a -> bw a -> b
co-Kleisli 箭头的 Fish 算子的模拟定义为:
The analog of the fish operator for co-Kleisli arrows is defined as:
(=>=) :: (w a -> b) -> (w b -> c) -> (w a -> c)(=>=) :: (w a -> b) -> (w b -> c) -> (w a -> c)
为了使 co-Kleisli 箭头形成一个范畴,我们还必须有一个恒等 co-Kleisli 箭头,称为extract:
For co-Kleisli arrows to form a category we also have to have an identity co-Kleisli arrow, which is called extract:
extract :: w a -> aextract :: w a -> a
这是 的对偶return。我们还必须施加结合律以及左恒等式和右恒等式。将它们放在一起,我们可以在 Haskell 中将 comonad 定义为:
This is the dual of return. We also have to impose the laws of associativity as well as left- and right-identity. Putting it all together, we could define a comonad in Haskell as:
class Functor w => Comonad w where
(=>=) :: (w a -> b) -> (w b -> c) -> (w a -> c)
extract :: w a -> aclass Functor w => Comonad w where
(=>=) :: (w a -> b) -> (w b -> c) -> (w a -> c)
extract :: w a -> a
在实践中,我们使用略有不同的原语,我们很快就会看到。
In practice, we use slightly different primitives, as we’ll see shortly.
问题是,comonad 在编程中有什么用?
The question is, what’s the use for comonads in programming?
让我们将 monad 与 comonad 进行比较。monad 提供了一种使用 将值放入容器的方法return。它不允许您访问存储在其中的一个或多个值。当然,实现单子的数据结构可能会提供对其内容的访问,但这被认为是一个好处。没有用于从 monad 中提取值的通用接口。我们已经看到了 monad 的例子IO,它以从不公开其内容而自豪。
Let’s compare the monad with the comonad. A monad provides a way of putting a value in a container using return. It doesn’t give you access to a value or values stored inside. Of course, data structures that implement monads might provide access to their contents, but that’s considered a bonus. There is no common interface for extracting values from a monad. And we’ve seen the example of the IO monad that prides itself in never exposing its contents.
另一方面,comonad 提供了从中提取单个值的方法。它没有提供插入值的方法。因此,如果您想将 comonad 视为一个容器,它总是预先填充有内容,并且可以让您查看它。
A comonad, on the other hand, provides the means of extracting a single value from it. It does not give the means to insert values. So if you want to think of a comonad as a container, it always comes pre-filled with contents, and it lets you peek at it.
就像 Kleisli 箭头接受一个值并产生一些修饰的结果一样——它用上下文来修饰它——co-Kleisli 箭头接受一个值和整个上下文并产生一个结果。它是上下文计算的一个体现。
Just as a Kleisli arrow takes a value and produces some embellished result — it embellishes it with context — a co-Kleisli arrow takes a value together with a whole context and produces a result. It’s an embodiment of contextual computation.
还记得读者单子吗?我们引入它是为了解决实现需要访问某些只读环境的计算的问题e。此类计算可以表示为以下形式的纯函数:
Remember the reader monad? We introduced it to tackle the problem of implementing computations that need access to some read-only environment e. Such computations can be represented as pure functions of the form:
(a, e) -> b(a, e) -> b
我们使用柯里化将它们变成 Kleisli 箭头:
We used currying to turn them into Kleisli arrows:
a -> (e -> b)a -> (e -> b)
但请注意,这些函数已经具有 co-Kleisli 箭头的形式。让我们将他们的论点转化为更方便的函子形式:
But notice that these functions already have the form of co-Kleisli arrows. Let’s massage their arguments into the more convenient functor form:
data Product e a = P e a
deriving Functordata Product e a = P e a
deriving Functor
我们可以通过为我们正在组合的箭头提供相同的环境来轻松定义组合运算符:
We can easily define the composition operator by making the same environment available to the arrows that we are composing:
(=>=) :: (Product e a -> b) -> (Product e b -> c) -> (Product e a -> c)
f =>= g = \(P e a) -> let b = f (P e a)
c = g (P e b)
in c(=>=) :: (Product e a -> b) -> (Product e b -> c) -> (Product e a -> c)
f =>= g = \(P e a) -> let b = f (P e a)
c = g (P e b)
in c
的实现extract简单地忽略了环境:
The implementation of extract simply ignores the environment:
extract (P e a) = aextract (P e a) = a
毫不奇怪,乘积 comonad 可用于执行与 reader monad 完全相同的计算。在某种程度上,环境的共模实现更加自然——它遵循“上下文计算”的精神。另一方面,单子带有方便的符号语法糖do。
Not surprisingly, the product comonad can be used to perform exactly the same computations as the reader monad. In a way, the comonadic implementation of the environment is more natural — it follows the spirit of “computation in context.” On the other hand, monads come with the convenient syntactic sugar of the do notation.
读者单子和乘积函子之间的联系更加深入,这与读者函子是乘积函子的右伴随这一事实有关。但总的来说,comonad 涵盖了与 monad 不同的计算概念。稍后我们会看到更多示例。
The connection between the reader monad and the product comonad goes deeper, having to do with the fact that the reader functor is the right adjoint of the product functor. In general, though, comonads cover different notions of computation than monads. We’ll see more examples later.
很容易将Productcomonad 推广到任意产品类型,包括元组和记录。
It’s easy to generalize the Product comonad to arbitrary product types including tuples and records.
继续二元化过程,我们可以继续对元绑定和连接进行二元化。或者,我们可以重复我们在 monad 中使用的过程,我们研究了鱼操作符的解剖结构。这种方法似乎更有启发性。
Continuing the process of dualization, we could go ahead and dualize monadic bind and join. Alternatively, we can repeat the process we used with monads, where we studied the anatomy of the fish operator. This approach seems more enlightening.
出发点是认识到组合运算符必须产生一个 co-Kleisli 箭头,该箭头接受w a并产生一个c。生成 a 的唯一方法c是将第二个函数应用于 类型的参数w b:
The starting point is the realization that the composition operator must produce a co-Kleisli arrow that takes w a and produces a c. The only way to produce a c is to apply the second function to an argument of the type w b:
(=>=) :: (w a -> b) -> (w b -> c) -> (w a -> c)
f =>= g = g ... (=>=) :: (w a -> b) -> (w b -> c) -> (w a -> c)
f =>= g = g ...
w b但是我们怎样才能产生一个可以输入的类型值呢g?我们可以使用 type 参数w a和 function f :: w a -> b。解决方案是定义bind的对偶,称为extend:
But how can we produce a value of type w b that could be fed to g? We have at our disposal the argument of type w a and the function f :: w a -> b. The solution is to define the dual of bind, which is called extend:
extend :: (w a -> b) -> w a -> w bextend :: (w a -> b) -> w a -> w b
使用extend我们可以实现组合:
Using extend we can implement composition:
f =>= g = g . extend ff =>= g = g . extend f
我们接下来可以解剖一下吗extend?您可能会想说,为什么不直接将函数应用于w a -> b参数w a,但您很快就会意识到无法将结果转换b为w b。请记住,comonad 不提供提升价值的方法。此时,在单子的类似构造中,我们使用了fmap. 我们在这里可以使用的唯一方法fmap是如果我们有某种类型的东西可供我们w (w a)使用。如果我们只能w a变成w (w a). 而且,方便地说,这正是 的对偶join。我们称之为duplicate:
Can we next dissect extend? You might be tempted to say, why not just apply the function w a -> b to the argument w a, but then you quickly realize that you’d have no way of converting the resulting b to w b. Remember, the comonad provides no means of lifting values. At this point, in the analogous construction for monads, we used fmap. The only way we could use fmap here would be if we had something of the type w (w a) at our disposal. If we coud only turn w a into w (w a). And, conveniently, that would be exactly the dual of join. We call it duplicate:
duplicate :: w a -> w (w a)duplicate :: w a -> w (w a)
因此,就像 monad 的定义一样,我们对 comonad 有三个等效的定义:使用 co-Kleisli 箭头、extend、 或duplicate。这是直接从库中获取的 Haskell 定义Control.Comonad:
So, just like with the definitions of the monad, we have three equivalent definitions of the comonad: using co-Kleisli arrows, extend, or duplicate. Here’s the Haskell definition taken directly from Control.Comonad library:
class Functor w => Comonad w where
extract :: w a -> a
duplicate :: w a -> w (w a)
duplicate = extend id
extend :: (w a -> b) -> w a -> w b
extend f = fmap f . duplicateclass Functor w => Comonad w where
extract :: w a -> a
duplicate :: w a -> w (w a)
duplicate = extend id
extend :: (w a -> b) -> w a -> w b
extend f = fmap f . duplicate
extend提供了in的默认实现,duplicate反之亦然,因此您只需覆盖其中之一。
Provided are the default implementations of extend in terms of duplicate and vice versa, so you only need to override one of them.
这些函数背后的直觉基于这样的想法:一般来说,comonad 可以被认为是一个充满类型值的容器a(产品 comonad 是只有一个值的特殊情况)。有一种“当前”值的概念,可以通过 轻松访问extract。co-Kleisli 箭头执行一些专注于当前值的计算,但它可以访问所有周围的值。想想康威的生命游戏。每个单元格包含一个值(通常只是True或False)。与生命游戏相对应的共生体将是专注于“当前”细胞的细胞网格。
The intuition behind these functions is based on the idea that, in general, a comonad can be thought of as a container filled with values of type a (the product comonad was a special case of just one value). There is a notion of the “current” value, one that’s easily accessible through extract. A co-Kleisli arrow performs some computation that is focused on the current value, but it has access to all the surrounding values. Think of the Conway’s game of life. Each cell contains a value (usually just True or False). A comonad corresponding to the game of life would be a grid of cells focused on the “current” cell.
那么它有什么duplicate作用呢?它需要一个共生容器w a并生成一个容器的容器w (w a)。这个想法是,每个容器都专注于不同的a内部w a。在生命游戏中,您将得到一个网格网格,外部网格的每个单元格都包含一个专注于不同单元格的内部网格。
So what does duplicate do? It takes a comonadic container w a and produces a container of containers w (w a). The idea is that each of these containers is focused on a different a inside w a. In the game of life, you would get a grid of grids, each cell of the outer grid containing an inner grid that’s focused on a different cell.
现在看看extend。它需要一个共同克莱斯利箭头和一个w a装满as 的共同容器。它将计算应用于所有这些as,并将它们替换为bs。结果是一个充满bs 的共生容器。extend通过将焦点从一个转移a到另一个并依次对每个焦点应用 co-Kleisli 箭头来实现这一点。在生命游戏中,co-Kleisli 箭头将计算当前细胞的新状态。为此,它会查看其上下文——大概是它最近的邻居。默认实现extend说明了这个过程。首先,我们调用duplicate产生所有可能的焦点,然后应用f到每个焦点。
Now look at extend. It takes a co-Kleisli arrow and a comonadic container w a filled with as. It applies the computation to all of these as, replacing them with bs. The result is a comonadic container filled with bs. extend does it by shifting the focus from one a to another and applying the co-Kleisli arrow to each of them in turn. In the game of life, the co-Kleisli arrow would calculate the new state of the current cell. To do that, it would look at its context — presumably its nearest neighbors. The default implementation of extend illustrates this process. First we call duplicate to produce all possible foci and then we apply f to each of them.
将焦点从容器的一个元素转移到另一个元素的过程可以通过无限流的示例得到最好的说明。这样的流就像一个列表,只是它没有空的构造函数:
This process of shifting the focus from one element of the container to another is best illustrated with the example of an infinite stream. Such a stream is just like a list, except that it doesn’t have the empty constructor:
data Stream a = Cons a (Stream a)data Stream a = Cons a (Stream a)
这很简单Functor:
It’s trivially a Functor:
instance Functor Stream where
fmap f (Cons a as) = Cons (f a) (fmap f as)instance Functor Stream where
fmap f (Cons a as) = Cons (f a) (fmap f as)
流的焦点是它的第一个元素,所以这里是它的实现extract:
The focus of a stream is its first element, so here’s the implementation of extract:
extract (Cons a _) = aextract (Cons a _) = a
duplicate产生一个流的流,每个流专注于不同的元素。
duplicate produces a stream of streams, each focused on a different element.
duplicate (Cons a as) = Cons (Cons a as) (duplicate as)duplicate (Cons a as) = Cons (Cons a as) (duplicate as)
第一个元素是原始流,第二个元素是原始流的尾部,第三个元素是其尾部,依此类推,无穷无尽。
The first element is the original stream, the second element is the tail of the original stream, the third element is its tail, and so on, ad infinitum.
这是完整的实例:
Here’s the complete instance:
instance Comonad Stream where
extract (Cons a _) = a
duplicate (Cons a as) = Cons (Cons a as) (duplicate as)instance Comonad Stream where
extract (Cons a _) = a
duplicate (Cons a as) = Cons (Cons a as) (duplicate as)
这是查看流的一种非常实用的方式。advance在命令式语言中,我们可能会从一种将流移动一个位置的方法开始。在这里,duplicate一次性产生所有移位的流。哈斯克尔的懒惰使得这成为可能,甚至是可取的。当然,为了实用Stream,我们还可以实现以下模拟advance:
This is a very functional way of looking at streams. In an imperative language, we would probably start with a method advance that shifts the stream by one position. Here, duplicate produces all shifted streams in one fell swoop. Haskell’s laziness makes this possible and even desirable. Of course, to make a Stream practical, we would also implement the analog of advance:
tail :: Stream a -> Stream a
tail (Cons a as) = astail :: Stream a -> Stream a
tail (Cons a as) = as
但它从来不是共模接口的一部分。
but it’s never part of the comonadic interface.
如果您有数字信号处理方面的经验,您将立即看到流的 co-Kleisli 箭头只是一个数字滤波器,并extend生成过滤后的流。
If you had any experience with digital signal processing, you’ll see immediately that a co-Kleisli arrow for a stream is just a digital filter, and extend produces a filtered stream.
作为一个简单的例子,让我们实现移动平均滤波器。n这是一个对流元素求和的函数:
As a simple example, let’s implement the moving average filter. Here’s a function that sums n elements of a stream:
sumS :: Num a => Int -> Stream a -> a
sumS n (Cons a as) = if n <= 0 then 0 else a + sumS (n - 1) assumS :: Num a => Int -> Stream a -> a
sumS n (Cons a as) = if n <= 0 then 0 else a + sumS (n - 1) as
n这是计算流的第一个元素的平均值的函数:
Here’s the function that calculates the average of the first n elements of the stream:
average :: Fractional a => Int -> Stream a -> a
average n stm = (sumS n stm) / (fromIntegral n)average :: Fractional a => Int -> Stream a -> a
average n stm = (sumS n stm) / (fromIntegral n)
部分应用的average n是 co-Kleisli 箭头,因此我们可以将extend其应用于整个流:
Partially applied average n is a co-Kleisli arrow, so we can extend it over the whole stream:
movingAvg :: Fractional a => Int -> Stream a -> Stream a
movingAvg n = extend (average n)movingAvg :: Fractional a => Int -> Stream a -> Stream a
movingAvg n = extend (average n)
结果是运行平均值流。
The result is the stream of running averages.
流是单向、一维共模的一个示例。它可以很容易地实现双向或扩展到两个或多个维度。
A stream is an example of a unidirectional, one-dimensional comonad. It can be easily made bidirectional or extended to two or more dimensions.
在范畴论中定义共元是对偶性的直接练习。与 monad 一样,我们从 endofunctor 开始T。定义单子的两个自然变换 η 和 μ 对于共单子来说只是相反:
Defining a comonad in category theory is a straightforward exercise in duality. As with the monad, we start with an endofunctor T. The two natural transformations, η and μ, that define the monad are simply reversed for the comonad:
ε :: T -> I
δ :: T -> T2ε :: T -> I
δ :: T -> T2
这些变换的分量对应于extract和duplicate。Comonad 定律是 monad 定律的镜像。这里没什么大惊喜。
The components of these transformations correspond to extract and duplicate. Comonad laws are the mirror image of monad laws. No big surprise here.
然后是从附加词衍生出单子。对偶性反转伴随:左伴随变成右伴随,反之亦然。并且,由于组合R ∘ L定义了一个 monad,L ∘ R因此必须定义一个 comonad。附加词的单位:
Then there is the derivation of the monad from an adjunction. Duality reverses an adjunction: the left adjoint becomes the right adjoint and vice versa. And, since the composition R ∘ L defines a monad, L ∘ R must define a comonad. The counit of the adjunction:
ε :: L ∘ R -> Iε :: L ∘ R -> I
确实与我们在 comonad 的定义中看到的 ε 相同,或者在组件中,与 Haskell 的 ε 相同extract。我们还可以使用附加词的单位:
is indeed the same ε that we see in the definition of the comonad — or, in components, as Haskell’s extract. We can also use the unit of the adjunction:
η :: I -> R ∘ Lη :: I -> R ∘ L
R ∘ L在 中间插入L ∘ R并生成L ∘ R ∘ L ∘ R. T2from定义了Tδ,这就完成了 comonad 的定义。
to insert an R ∘ L in the middle of L ∘ R and produce L ∘ R ∘ L ∘ R. Making T2 from T defines the δ, and that completes the definition of the comonad.
我们还看到 monad 是一个幺半群。该语句的对偶需要使用 comonoid,那么什么是 comonoid?幺半群的最初定义是单一对象范畴,并没有对偶化任何有趣的东西。当你反转所有自同态的方向时,你会得到另一个幺半群。然而,回想一下,在我们的 monad 方法中,我们使用了更一般的 monoid 定义作为 monoidal 范畴中的对象。该构造基于两个态射:
We’ve also seen that the monad is a monoid. The dual of this statement would require the use of a comonoid, so what’s a comonoid? The original definition of a monoid as a single-object category doesn’t dualize to anything interesting. When you reverse the direction of all endomorphisms, you get another monoid. Recall, however, that in our approach to a monad, we used a more general definition of a monoid as an object in a monoidal category. The construction was based on two morphisms:
μ :: m ⊗ m -> m
η :: i -> mμ :: m ⊗ m -> m
η :: i -> m
这些态射的反转在幺半群范畴中产生了一个共同群:
The reversal of these morphisms produces a comonoid in a monoidal category:
δ :: m -> m ⊗ m
ε :: m -> iδ :: m -> m ⊗ m
ε :: m -> i
人们可以用 Haskell 写出 comonoid 的定义:
One can write a definition of a comonoid in Haskell:
class Comonoid m where
split :: m -> (m, m)
destroy :: m -> ()class Comonoid m where
split :: m -> (m, m)
destroy :: m -> ()
但这是微不足道的。显然destroy忽略了它的论点。
but it is rather trivial. Obviously destroy ignores its argument.
destroy _ = ()destroy _ = ()
split只是一对函数:
split is just a pair of functions:
split x = (f x, g x)split x = (f x, g x)
现在考虑与幺半群单位定律对偶的共群定律。
Now consider comonoid laws that are dual to the monoid unit laws.
lambda . bimap destroy id . split = id
rho . bimap id destroy . split = idlambda . bimap destroy id . split = id
rho . bimap id destroy . split = id
这里,lambda和分别是左单位和右单位(参见幺半群范畴rho的定义)。代入定义,我们得到:
Here, lambda and rho are the left and right unitors, respectively (see the definition of monoidal categories). Plugging in the definitions, we get:
lambda (bimap destroy id (split x))
= lambda (bimap destroy id (f x, g x))
= lambda ((), g x)
= g xlambda (bimap destroy id (split x))
= lambda (bimap destroy id (f x, g x))
= lambda ((), g x)
= g x
这证明了g = id。类似地,第二定律扩展到f = id。综上所述:
which proves that g = id. Similarly, the second law expands to f = id. In conclusion:
split x = (x, x)split x = (x, x)
这表明在 Haskell 中(以及一般情况下在Set范畴中)每个对象都是一个平凡的 comonoid。
which shows that in Haskell (and, in general, in the category Set) every object is a trivial comonoid.
幸运的是,还有其他更有趣的幺半群范畴可以用来定义共同群。其中之一是内函子范畴。事实证明,就像单子是内函子范畴中的幺半群一样,
Fortunately there are other more interesting monoidal categories in which to define comonoids. One of them is the category of endofunctors. And it turns out that, just like the monad is a monoid in the category of endofunctors,
comonad 是内函子范畴中的一个 comonoid。
The comonad is a comonoid in the category of endofunctors.
共生体的另一个重要例子是状态单子的对偶。它被称为 costate comonad 或 store comonad。
Another important example of a comonad is the dual of the state monad. It’s called the costate comonad or, alternatively, the store comonad.
我们之前已经看到,状态单子是由定义指数的附加项生成的:
We’ve seen before that the state monad is generated by the adjunction that defines the exponentials:
L z = z × s
R a = s ⇒ aL z = z × s
R a = s ⇒ a
我们将使用相同的附加词来定义肋comonad。共同点由以下组成定义L ∘ R:
We’ll use the same adjunction to define the costate comonad. A comonad is defined by the composition L ∘ R:
L (R a) = (s ⇒ a) × sL (R a) = (s ⇒ a) × s
Prod将其转换为 Haskell,我们从左侧函子和Reader右侧函子之间的附加开始。Prod之后编写Reader相当于以下定义:
Translating this to Haskell, we start with the adjunction between the Prod functor on the left and the Reader functor or the right. Composing Prod after Reader is equivalent to the following definition:
data Store s a = Store (s -> a) sdata Store s a = Store (s -> a) s
对对象进行的附加的计数a是态射:
The counit of the adjunction taken at the object a is the morphism:
εa :: ((s ⇒ a) × s) -> aεa :: ((s ⇒ a) × s) -> a
或者,用 Haskell 表示法:
or, in Haskell notation:
counit (Prod (Reader f, s)) = f scounit (Prod (Reader f, s)) = f s
这成为我们的extract:
This becomes our extract:
extract (Store f s) = f sextract (Store f s) = f s
附加单位:
The unit of the adjunction:
unit a = Reader (\s -> Prod (a, s))unit a = Reader (\s -> Prod (a, s))
可以重写为部分应用的数据构造函数:
can be rewritten as partially applied data constructor:
Store f :: s -> Store f sStore f :: s -> Store f s
我们构造 δ 或duplicate,作为水平组合:
We construct δ, or duplicate, as the horizontal composition:
δ :: L ∘ R -> L ∘ R ∘ L ∘ R
δ = L ∘ η ∘ Rδ :: L ∘ R -> L ∘ R ∘ L ∘ R
δ = L ∘ η ∘ R
我们必须通过最左边的L,也就是Prod函子来潜入 η 。这意味着用 η 或 作用Store f于该对的左侧分量(这就是fmapforProd的作用)。我们得到:
We have to sneak η through the leftmost L, which is the Prod functor. It means acting with η, or Store f, on the left component of the pair (that’s what fmap for Prod would do). We get:
duplicate (Store f s) = Store (Store f) sduplicate (Store f s) = Store (Store f) s
(请记住,在 δ 的公式中,L和R代表恒等自然变换,其组成部分是恒等态射。)
(Remember that, in the formula for δ, L and R stand for identity natural transformations whose components are identity morphisms.)
这是 comonad 的完整定义Store:
Here’s the complete definition of the Store comonad:
instance Comonad (Store s) where
extract (Store f s) = f s
duplicate (Store f s) = Store (Store f) sinstance Comonad (Store s) where
extract (Store f s) = f s
duplicate (Store f s) = Store (Store f) s
您可以将Reader的 部分视为 sStore的通用容器a,使用 类型的元素作为键控s。例如,如果sis Int,Reader Int a则为 s 的无限双向流a。Store将此容器与键类型的值配对。例如,Reader Int a与 配对Int。在这种情况下,extract使用该整数来索引无限流。您可以将 的第二个组成部分视为Store当前位置。
You may think of the Reader part of Store as a generalized container of as that are keyed using elements of the type s. For instance, if s is Int, Reader Int a is an infinite bidirectional stream of as. Store pairs this container with a value of the key type. For instance, Reader Int a is paired with an Int. In this case, extract uses this integer to index into the infinite stream. You may think of the second component of Store as the current position.
继续此示例,duplicate创建一个由Int. 该流包含流作为其元素。特别是,在当前位置,它包含原始流。但是,如果您使用其他一些Int(正或负)作为键,您将获得位于该新索引处的移位流。
Continuing with this example, duplicate creates a new infinite stream indexed by an Int. This stream contains streams as its elements. In particular, at the current position, it contains the original stream. But if you use some other Int (positive or negative) as the key, you’d obtain a shifted stream positioned at that new index.
一般来说,你可以说服自己,extract当作用于duplicated时Store,它会产生原始的Store(事实上,comonad 的恒等律指出extract . duplicate = id)。
In general, you can convince yourself that when extract acts on the duplicated Store it produces the original Store (in fact, the identity law for the comonad states that extract . duplicate = id).
共生Store体作为图书馆的理论基础发挥着重要作用Lens。从概念上讲, comonad 封装了使用类型Store s a作为索引“聚焦”(像镜头一样)日期类型的特定子结构的想法。特别是以下类型的函数:as
The Store comonad plays an important role as the theoretical basis for the Lens library. Conceptually, the Store s a comonad encapsulates the idea of “focusing” (like a lens) on a particular substructure of the date type a using the type s as an index. In particular, a function of the type:
a -> Store s aa -> Store s a
相当于一对函数:
is equivalent to a pair of functions:
set :: a -> s -> a
get :: a -> sset :: a -> s -> a
get :: a -> s
如果a是产品类型,可以通过设置内部set的 type 字段来实现,同时返回 的修改版本。类似地,可以实现从 读取字段的值。我们将在下一节中更多地探讨这些想法。saagetsa
If a is a product type, set could be implemented as setting the field of type s inside of a while returning the modified version of a. Similarly, get could be implemented to read the value of the s field from a. We’ll explore these ideas more in the next section.
Store。提示:您选择什么类型s?Store comonad. Hint: What type do you pick for s?我很感谢 Edward Kmett 阅读了这篇文章的草稿并指出了我推理中的缺陷。
I’m grateful to Edward Kmett for reading the draft of this post and pointing out flaws in my reasoning.
我们已经看到了幺半群的几种表述:作为一个集合,作为一个单一对象范畴,作为一个幺半群范畴中的对象。我们还能从这个简单的概念中榨取多少汁液呢?
We’ve seen several formulations of a monoid: as a set, as a single-object category, as an object in a monoidal category. How much more juice can we squeeze out of this simple concept?
咱们试试吧。将幺半群的定义视为m具有一对函数的集合:
Let’s try. Take this definition of a monoid as a set m with a pair of functions:
μ :: m × m -> m
η :: 1 -> mμ :: m × m -> m
η :: 1 -> m
这里,1是Set中的终端对象——单例集合。第一个函数定义乘法(它接受一对元素并返回它们的乘积),第二个函数从 中选择单位元素m。并非每次选择具有这些签名的两个函数都会产生幺半群。为此,我们需要施加额外的条件:结合律和单位定律。但让我们暂时忘记这一点,只考虑“潜在的幺半群”。函数对是两组函数的笛卡尔积的元素。我们知道这些集合可以表示为指数对象:
Here, 1 is the terminal object in Set — the singleton set. The first function defines multiplication (it takes a pair of elements and returns their product), the second selects the unit element from m. Not every choice of two functions with these signatures results in a monoid. For that we need to impose additional conditions: associativity and unit laws. But let’s forget about that for a moment and just consider “potential monoids.” A pair of functions is an element of a cartesian product of two sets of functions. We know that these sets may be represented as exponential objects:
μ ∈ m m×m
η ∈ m1μ ∈ m m×m
η ∈ m1
这两组的笛卡尔积为:
The cartesian product of these two sets is:
m m×m × m1m m×m × m1
使用一些高中代数(适用于每个笛卡尔封闭范畴),我们可以将其重写为:
Using some high-school algebra (which works in every cartesian closed category), we can rewrite it as:
m m×m + 1m m×m + 1
加号代表Set中的余积。我们刚刚用一个函数(集合的一个元素)替换了一对函数:
The plus sign stands for the coproduct in Set. We have just replaced a pair of functions with a single function — an element of the set:
m × m + 1 -> mm × m + 1 -> m
这组函数的任何元素都是潜在的幺半群。
Any element of this set of functions is a potential monoid.
这个公式的美妙之处在于它可以得出有趣的概括。例如,我们如何使用这种语言描述一个群体?群是一个幺半群,具有一个附加函数,该函数将逆元分配给每个元素。后者是 type 的函数m->m。例如,整数形成一个群,加法作为二元运算,零作为单位,负作为逆元。为了定义一个组,我们将从三个函数开始:
The beauty of this formulation is that it leads to interesting generalizations. For instance, how would we describe a group using this language? A group is a monoid with one additional function that assigns the inverse to every element. The latter is a function of the type m->m. As an example, integers form a group with addition as a binary operation, zero as the unit, and negation as the inverse. To define a group we would start with a triple of functions:
m × m -> m
m -> m
1 -> mm × m -> m
m -> m
1 -> m
和以前一样,我们可以将所有这些三元组组合成一组函数:
As before, we can combine all these triples into one set of functions:
m × m + m + 1 -> mm × m + m + 1 -> m
我们从一个二元运算符(加法)、一个一元运算符(求反)和一个零运算符(同一性 - 这里为零)开始。我们将它们合并为一个函数。具有此签名的所有函数都定义了潜在的组。
We started with one binary operator (addition), one unary operator (negation), and one nullary operator (identity — here zero). We combined them into one function. All functions with this signature define potential groups.
我们可以这样继续下去。例如,要定义一个环,我们将添加一个二元运算符和一个空运算符,依此类推。每次我们都会得到一个函数类型,其左侧是幂的总和(可能包括零次幂 - 终结对象),右侧是集合本身。
We can go on like this. For instance, to define a ring, we would add one more binary operator and one nullary operator, and so on. Each time we end up with a function type whose left-hand side is a sum of powers (possibly including the zeroth power — the terminal object), and the right-hand side being the set itself.
现在我们可以疯狂地进行概括。首先,我们可以用对象代替集合,用态射代替函数。我们可以将 n 元运算符定义为 n 元乘积的态射。这意味着我们需要一个支持有限产品的范畴。对于零运算符,我们要求存在终端对象。所以我们需要一个笛卡尔范畴。为了组合这些运算符,我们需要指数,所以这是一个笛卡尔封闭范畴。最后,我们需要余积来完成我们的代数恶作剧。
Now we can go crazy with generalizations. First of all, we can replace sets with objects and functions with morphisms. We can define n-ary operators as morphisms from n-ary products. It means that we need a category that supports finite products. For nullary operators we require the existence of the terminal object. So we need a cartesian category. In order to combine these operators we need exponentials, so that’s a cartesian closed category. Finally, we need coproducts to complete our algebraic shenanigans.
或者,我们可以忘记公式的推导方式,而专注于最终产品。态射左侧的乘积之和定义了一个内函子。如果我们选择一个任意的endofunctor 呢F?在这种情况下,我们不必对我们的范畴施加任何限制。我们得到的称为F-代数。
Alternatively, we can just forget about the way we derived our formulas and concentrate on the final product. The sum of products on the left hand side of our morphism defines an endofunctor. What if we pick an arbitrary endofunctor F instead? In that case we don’t have to impose any constraints on our category. What we obtain is called an F-algebra.
F 代数是由内函子F、对象a和态射组成的三元组
An F-algebra is a triple consisting of an endofunctor F, an object a, and a morphism
F a -> aF a -> a
该对象通常称为载体、底层对象,或者在编程上下文中称为载体类型。态射通常称为评价函数或结构图。将函子F视为形成表达式,将态射视为评估它们。
The object is often called the carrier, an underlying object or, in the context of programming, the carrier type. The morphism is often called the evaluation function or the structure map. Think of the functor F as forming expressions and the morphism as evaluating them.
这是 F 代数的 Haskell 定义:
Here’s the Haskell definition of an F-algebra:
type Algebra f a = f a -> atype Algebra f a = f a -> a
它用其评估函数来识别代数。
It identifies the algebra with its evaluation function.
在幺半群示例中,所讨论的函子是:
In the monoid example, the functor in question is:
data MonF a = MEmpty | MAppend a adata MonF a = MEmpty | MAppend a a
这是 Haskell 1 + a × a(记住代数数据结构)。
This is Haskell for 1 + a × a (remember algebraic data structures).
环将使用以下函子定义:
A ring would be defined using the following functor:
data RingF a = RZero
| ROne
| RAdd a a
| RMul a a
| RNeg adata RingF a = RZero
| ROne
| RAdd a a
| RMul a a
| RNeg a
这是 Haskell 的1 + 1 + a × a + a × a + a.
which is Haskell for 1 + 1 + a × a + a × a + a.
环的一个例子是整数集。我们可以选择Integer载体类型并将评估函数定义为:
An example of a ring is the set of integers. We can choose Integer as the carrier type and define the evaluation function as:
evalZ :: Algebra RingF Integer
evalZ RZero = 0
evalZ ROne = 1
evalZ (RAdd m n) = m + n
evalZ (RMul m n) = m * n
evalZ (RNeg n) = -nevalZ :: Algebra RingF Integer
evalZ RZero = 0
evalZ ROne = 1
evalZ (RAdd m n) = m + n
evalZ (RMul m n) = m * n
evalZ (RNeg n) = -n
基于相同函子的 F 代数有更多RingF。例如,多项式形成一个环,方阵也是如此。
There are more F-algebras based on the same functor RingF. For instance, polynomials form a ring and so do square matrices.
正如您所看到的,函子的作用是生成可以使用代数求值器求值的表达式。到目前为止我们只看到了非常简单的表达式。我们通常对可以使用递归定义的更复杂的表达式感兴趣。
As you can see, the role of the functor is to generate expressions that can be evaluated using the evaluator of the algebra. So far we’ve only seen very simple expressions. We are often interested in more elaborate expressions that can be defined using recursion.
生成任意表达式树的一种方法是用a递归替换函子定义中的变量。例如,环中的任意表达式都是由这种树状数据结构生成的:
One way to generate arbitrary expression trees is to replace the variable a inside the functor definition with recursion. For instance, an arbitrary expression in a ring is generated by this tree-like data structure:
data Expr = RZero
| ROne
| RAdd Expr Expr
| RMul Expr Expr
| RNeg Exprdata Expr = RZero
| ROne
| RAdd Expr Expr
| RMul Expr Expr
| RNeg Expr
我们可以用递归版本替换原来的环求值器:
We can replace the original ring evaluator with its recursive version:
evalZ :: Expr -> Integer
evalZ RZero = 0
evalZ ROne = 1
evalZ (RAdd e1 e2) = evalZ e1 + evalZ e2
evalZ (RMul e1 e2) = evalZ e1 * evalZ e2
evalZ (RNeg e) = -(evalZ e)evalZ :: Expr -> Integer
evalZ RZero = 0
evalZ ROne = 1
evalZ (RAdd e1 e2) = evalZ e1 + evalZ e2
evalZ (RMul e1 e2) = evalZ e1 * evalZ e2
evalZ (RNeg e) = -(evalZ e)
这仍然不太实用,因为我们被迫将所有整数表示为一之和,但在紧要关头它就可以了。
This is still not very practical, since we are forced to represent all integers as sums of ones, but it will do in a pinch.
但是我们如何使用 F 代数语言来描述表达式树呢?我们必须以某种方式形式化用替换结果递归地替换函子定义中的自由类型变量的过程。想象一下分步骤执行此操作。首先,将深度一树定义为:
But how can we describe expression trees using the language of F-algebras? We have to somehow formalize the process of replacing the free type variable in the definition of our functor, recursively, with the result of the replacement. Imagine doing this in steps. First, define a depth-one tree as:
type RingF1 a = RingF (RingF a)type RingF1 a = RingF (RingF a)
RingF我们正在用 生成的零深度树来填补 的定义中的漏洞RingF a。深度 2 树的获取方式类似:
We are filling the holes in the definition of RingF with depth-zero trees generated by RingF a. Depth-2 trees are similarly obtained as:
type RingF2 a = RingF (RingF (RingF a))type RingF2 a = RingF (RingF (RingF a))
我们也可以写成:
which we can also write as:
type RingF2 a = RingF (RingF1 a)type RingF2 a = RingF (RingF1 a)
继续这个过程,我们可以写出一个符号方程:
Continuing this process, we can write a symbolic equation:
type RingFn+1 a = RingF (RingFn a)type RingFn+1 a = RingF (RingFn a)
从概念上讲,在无数次重复这个过程之后,我们最终得到了我们的Expr. 请注意,它Expr不依赖于a. 旅程的起点并不重要,我们总会到达同一个地方。对于任意范畴中的任意末端函子来说,情况并不总是如此,但在集合范畴中,情况很好。
Conceptually, after repeating this process infinitely many times, we end up with our Expr. Notice that Expr does not depend on a. The starting point of our journey doesn’t matter, we always end up in the same place. This is not always true for an arbitrary endofunctor in an arbitrary category, but in the category Set things are nice.
当然,这是一种挥手的说法,稍后我会使其更加严谨。
Of course, this is a hand-waving argument, and I’ll make it more rigorous later.
无限次应用 endofunctor 会产生一个不动点,该对象定义为:
Applying an endofunctor infinitely many times produces a fixed point, an object defined as:
Fix f = f (Fix f)Fix f = f (Fix f)
这个定义背后的直觉是,由于我们f无限次地应用 get Fix f,所以再应用一次不会改变任何东西。在 Haskell 中,不动点的定义是:
The intuition behind this definition is that, since we applied f infinitely many times to get Fix f, applying it one more time doesn’t change anything. In Haskell, the definition of a fixed point is:
newtype Fix f = Fix (f (Fix f))newtype Fix f = Fix (f (Fix f))
可以说,如果构造函数的名称与所定义的类型的名称不同,这会更具可读性,如下所示:
Arguably, this would be more readable if the constructor’s name were different than the name of the type being defined, as in:
newtype Fix f = In (f (Fix f))newtype Fix f = In (f (Fix f))
但我会坚持使用公认的符号。构造函数Fix(或者In,如果您愿意的话)可以被视为一个函数:
but I’ll stick with the accepted notation. The constructor Fix (or In, if you prefer) can be seen as a function:
Fix :: f (Fix f) -> Fix fFix :: f (Fix f) -> Fix f
还有一个函数可以剥离函子应用的一层:
There is also a function that peels off one level of functor application:
unFix :: Fix f -> f (Fix f)
unFix (Fix x) = xunFix :: Fix f -> f (Fix f)
unFix (Fix x) = x
这两个函数互为反函数。稍后我们将使用这些函数。
The two functions are the inverse of each other. We’ll use these functions later.
这是书中最古老的技巧:每当你想出一种构造一些新对象的方法时,看看它们是否形成一个范畴。毫不奇怪,给定内函子上的代数F形成一个范畴。a该范畴中的对象是代数——由载体对象和态射组成的对F a -> a,两者都来自原始范畴C。
Here’s the oldest trick in the book: Whenever you come up with a way of constructing some new objects, see if they form a category. Not surprisingly, algebras over a given endofunctor F form a category. Objects in that category are algebras — pairs consisting of a carrier object a and a morphism F a -> a, both from the original category C.
为了完成这幅图,我们必须定义 F 代数范畴中的态射。态射必须将一个代数映射(a, f)到另一个代数(b, g)。我们将其定义为m映射载流子的态射——它在原始范畴中从a到。b不是任何态射都可以:我们希望它与两个求值器兼容。(我们将这种保留结构的态射称为同态。)以下是定义 F 代数同态的方法。首先,请注意我们可以提升m到映射:
To complete the picture, we have to define morphisms in the category of F-algebras. A morphism must map one algebra (a, f) to another algebra (b, g). We’ll define it as a morphism m that maps the carriers — it goes from a to b in the original category. Not any morphism will do: we want it to be compatible with the two evaluators. (We call such a structure-preserving morphism a homomorphism.) Here’s how you define a homomorphism of F-algebras. First, notice that we can lift m to the mapping:
F m :: F a -> F bF m :: F a -> F b
然后我们可以跟随它g到达b。同样,我们可以使用ffrom F ato a,然后使用m。我们希望两条路径相等:
we can then follow it with g to get to b. Equivalently, we can use f to go from F a to a and then follow it with m. We want the two paths to be equal:
g ∘ F m = m ∘ fg ∘ F m = m ∘ f
很容易让自己相信这确实是一个范畴(提示: C中的恒等态射工作得很好,同态的组合是同态)。
It’s easy to convince yourself that this is indeed a category (hint: identity morphisms from C work just fine, and a composition of homomorphisms is a homomorphism).
F-代数范畴中的初始对象如果存在,则称为初始代数。我们将这个初始代数的载体i及其求值器称为j :: F i -> i。事实证明j,初始代数的求值器 是一个同构。这个结果被称为兰贝克定理。m证明依赖于初始对象的定义,这要求它与任何其他 F 代数都存在唯一的同态。由于m是同态,所以下图必须可交换:
An initial object in the category of F-algebras, if it exists, is called the initial algebra. Let’s call the carrier of this initial algebra i and its evaluator j :: F i -> i. It turns out that j, the evaluator of the initial algebra, is an isomorphism. This result is known as Lambek’s theorem. The proof relies on the definition of the initial object, which requires that there be a unique homomorphism m from it to any other F-algebra. Since m is a homomorphism, the following diagram must commute:
现在让我们构造一个其载体为 的代数F i。这种代数的求值器必须是从F (F i)到 的态射F i。我们可以通过简单地提升来轻松构建这样的评估器j:
Now let’s construct an algebra whose carrier is F i. The evaluator of such an algebra must be a morphism from F (F i) to F i. We can easily construct such an evaluator simply by lifting j:
F j :: F (F i) -> F iF j :: F (F i) -> F i
因为(i, j)是初始代数,所以m从它到必定存在唯一的同态(F i, F j)。下图必须通勤:
Because (i, j) is the initial algebra, there must be a unique homomorphism m from it to (F i, F j). The following diagram must commute:
但我们还有这个简单的通勤图(两条路径是相同的!):
But we also have this trivially commuting diagram (both paths are the same!):
这可以解释为表明j是代数的同态,映射(F i, F j)到(i, j)。我们可以将这两个图粘在一起得到:
which can be interpreted as showing that j is a homomorphism of algebras, mapping (F i, F j) to (i, j). We can glue these two diagrams together to get:
反过来,该图可以解释为表明这j ∘ m是代数的同态。仅在这种情况下,两个代数是相同的。此外,因为(i, j)是初始的,所以从它到自身只能有一个同态,这就是恒等态射idi——我们知道它是代数的同态。因此j ∘ m = idi。利用这个事实和左图的通勤性质我们可以证明这一点m ∘ j = idFi。这表明m是 的逆j,因此是和j之间的同构:F ii
This diagram may, in turn, be interpreted as showing that j ∘ m is a homomorphism of algebras. Only in this case the two algebras are the same. Moreover, because (i, j) is initial, there can only be one homomorphism from it to itself, and that’s the identity morphism idi — which we know is a homomorphism of algebras. Therefore j ∘ m = idi. Using this fact and the commuting property of the left diagram we can prove that m ∘ j = idFi. This shows that m is the inverse of j and therefore j is an isomorphism between F i and i:
F i ≅ iF i ≅ i
但这只是说 是i的一个不动点F。这就是最初的挥手论证背后的正式证据。
But that is just saying that i is a fixed point of F. That’s the formal proof behind the original hand-waving argument.
回到 Haskell:我们将 视为i我们的构造函数Fix f,将其逆视为。兰贝克定理中的同构告诉我们,为了得到初始代数,我们采用函子并将其参数替换为。我们还看到为什么不动点不依赖于。jFixunFixfaFix fa
Back to Haskell: We recognize i as our Fix f, j as our constructor Fix, and its inverse as unFix. The isomorphism in Lambek’s theorem tells us that, in order to get the initial algebra, we take the functor f and replace its argument a with Fix f. We also see why the fixed point does not depend on a.
自然数也可以定义为 F 代数。起点是一对态射:
Natural numbers can also be defined as an F-algebra. The starting point is the pair of morphisms:
zero :: 1 -> N
succ :: N -> Nzero :: 1 -> N
succ :: N -> N
第一个选择零,第二个将所有数字映射到其后继者。和以前一样,我们可以将两者合二为一:
The first one picks the zero, and the second one maps all numbers to their successors. As before, we can combine the two into one:
1 + N -> N1 + N -> N
左边定义了一个函子,在 Haskell 中可以这样写:
The left hand side defines a functor which, in Haskell, can be written like this:
data NatF a = ZeroF | SuccF adata NatF a = ZeroF | SuccF a
该函子的不动点(它生成的初始代数)可以在 Haskell 中编码为:
The fixed point of this functor (the initial algebra that it generates) can be encoded in Haskell as:
data Nat = Zero | Succ Natdata Nat = Zero | Succ Nat
自然数要么为零,要么是另一个数的后继。这称为自然数的皮亚诺表示法。
A natural number is either zero or a successor of another number. This is known as the Peano representation for natural numbers.
让我们使用 Haskell 表示法重写初始化条件。我们称之为初始代数Fix f。它的求值者是构造者Fix。m对于同一函子,从初始代数到任何其他代数都存在独特的态射。让我们选择一个代数,其载体为a,求值器为alg。
Let’s rewrite the initiality condition using Haskell notation. We call the initial algebra Fix f. Its evaluator is the contructor Fix. There is a unique morphism m from the initial algebra to any other algebra over the same functor. Let’s pick an algebra whose carrier is a and the evaluator is alg.
顺便说一下,请注意它m是什么:它是定点的求值器,是整个递归表达式树的求值器。让我们找到实现它的通用方法。
By the way, notice what m is: It’s an evaluator for the fixed point, an evaluator for the whole recursive expression tree. Let’s find a general way of implementing it.
兰贝克定理告诉我们,构造函数Fix是同构的。我们称其为逆unFix。因此,我们可以翻转该图中的一个箭头来得到:
Lambek’s theorem tells us that the constructor Fix is an isomorphism. We called its inverse unFix. We can therefore flip one arrow in this diagram to get:
让我们写下该图的换向条件:
Let’s write down the commutation condition for this diagram:
m = alg . fmap m . unFixm = alg . fmap m . unFix
我们可以将这个方程解释为 的递归定义m。对于使用函子创建的任何有限树,递归必然终止f。我们可以通过注意到fmap m操作在函子顶层下面来看到这一点f。换句话说,它适用于原始树的子级。孩子们总是比原来的树浅一层。
We can interpret this equation as a recursive definition of m. The recursion is bound to terminate for any finite tree created using the functor f. We can see that by noticing that fmap m operates underneath the top layer of the functor f. In other words, it works on the children of the original tree. The children are always one level shallower than the original tree.
m当我们应用到使用 构造的树时,会发生以下情况Fix f。的操作unFix剥离了构造函数,暴露了树的顶层。然后我们应用m到顶部节点的所有子节点。这会产生 类型的结果a。最后,我们通过应用非递归求值器来组合这些结果alg。关键是我们的求值器alg是一个简单的非递归函数。
Here’s what happens when we apply m to a tree constructed using Fix f. The action of unFix peels off the constructor, exposing the top level of the tree. We then apply m to all the children of the top node. This produces results of type a. Finally, we combine those results by applying the non-recursive evaluator alg. The key point is that our evaluator alg is a simple non-recursive function.
由于我们可以对任何代数执行此操作alg,因此定义一个以代数作为参数并为我们提供所调用的函数的高阶函数是有意义的m。这个高阶函数称为变形:
Since we can do this for any algebra alg, it makes sense to define a higher order function that takes the algebra as a parameter and gives us the function we called m. This higher order function is called a catamorphism:
cata :: Functor f => (f a -> a) -> Fix f -> a
cata alg = alg . fmap (cata alg) . unFixcata :: Functor f => (f a -> a) -> Fix f -> a
cata alg = alg . fmap (cata alg) . unFix
让我们看一个例子。采用定义自然数的函子:
Let’s see an example of that. Take the functor that defines natural numbers:
data NatF a = ZeroF | SuccF adata NatF a = ZeroF | SuccF a
让我们选择(Int, Int)载体类型并将我们的代数定义为:
Let’s pick (Int, Int) as the carrier type and define our algebra as:
fib :: NatF (Int, Int) -> (Int, Int)
fib ZeroF = (1, 1)
fib (SuccF (m, n)) = (n, m + n)fib :: NatF (Int, Int) -> (Int, Int)
fib ZeroF = (1, 1)
fib (SuccF (m, n)) = (n, m + n)
您可以轻松地说服自己,该代数的变形计算 ,cata fib计算斐波那契数。
You can easily convince yourself that the catamorphism for this algebra, cata fib, calculates Fibonacci numbers.
一般来说,代数NatF定义了递归关系:当前元素的值与前一个元素的关系。然后,变形计算该序列的第 n 个元素。
In general, an algebra for NatF defines a recurrence relation: the value of the current element in terms of the previous element. A catamorphism then evaluates the n-th element of that sequence.
列表e是以下函子的初始代数:
A list of e is the initial algebra of the following functor:
data ListF e a = NilF | ConsF e adata ListF e a = NilF | ConsF e a
事实上,用递归的结果替换变量a(我们称之为 )List e,我们得到:
Indeed, replacing the variable a with the result of recursion, which we’ll call List e, we get:
data List e = Nil | Cons e (List e)data List e = Nil | Cons e (List e)
列表函子的代数选择特定的载体类型并定义一个对两个构造函数进行模式匹配的函数。它的值NilF告诉我们如何评估一个空列表,它的值ConsF告诉我们如何将当前元素与先前累积的值组合起来。
An algebra for a list functor picks a particular carrier type and defines a function that does pattern matching on the two constructors. Its value for NilF tells us how to evaluate an empty list, and its value for ConsF tells us how to combine the current element with the previously accumulated value.
例如,这是一个可用于计算列表长度的代数(载体类型为Int):
For instance, here’s an algebra that can be used to calculate the length of a list (the carrier type is Int):
lenAlg :: ListF e Int -> Int
lenAlg (ConsF e n) = n + 1
lenAlg NilF = 0lenAlg :: ListF e Int -> Int
lenAlg (ConsF e n) = n + 1
lenAlg NilF = 0
事实上,由此产生的变形cata lenAlg计算了列表的长度。请注意,求值器是 (1) 一个函数的组合,该函数接受列表元素和累加器并返回一个新累加器,以及 (2) 起始值(此处为零)。值的类型和累加器的类型由载体类型给出。
Indeed, the resulting catamorphism cata lenAlg calculates the length of a list. Notice that the evaluator is a combination of (1) a function that takes a list element and an accumulator and returns a new accumulator, and (2) a starting value, here zero. The type of the value and the type of the accumulator are given by the carrier type.
将其与传统的 Haskell 定义进行比较:
Compare this to the traditional Haskell definition:
length = foldr (\e n -> n + 1) 0length = foldr (\e n -> n + 1) 0
的两个参数foldr恰好是代数的两个组成部分。
The two arguments to foldr are exactly the two components of the algebra.
让我们尝试另一个例子:
Let’s try another example:
sumAlg :: ListF Double Double -> Double
sumAlg (ConsF e s) = e + s
sumAlg NilF = 0.0sumAlg :: ListF Double Double -> Double
sumAlg (ConsF e s) = e + s
sumAlg NilF = 0.0
再次将其与以下内容进行比较:
Again, compare this with:
sum = foldr (\e s -> e + s) 0.0sum = foldr (\e s -> e + s) 0.0
正如您所看到的,foldr这只是列表的变形的一种方便的特化。
As you can see, foldr is just a convenient specialization of a catamorphism to lists.
像往常一样,我们有一个 F-coagebra 的对偶构造,其中态射的方向是相反的:
As usual, we have a dual construction of an F-coagebra, where the direction of the morphism is reversed:
a -> F aa -> F a
给定函子的余代数也形成一个范畴,同态保留了余代数结构。该范畴中的终端对象(t, u)称为终端(或最终)余代数。对于所有其他代数,(a, f)都有一个独特的同态m,使得下图可通:
Coalgebras for a given functor also form a category, with homomorphisms preserving the coalgebraic structure. The terminal object (t, u) in that category is called the terminal (or final) coalgebra. For every other algebra (a, f) there is a unique homomorphism m that makes the following diagram commute:
终端拼贴是函子的不动点,从某种意义上说,态射u :: t -> F t是同构(兰贝克的余代数定理):
A terminal colagebra is a fixed point of the functor, in the sense that the morphism u :: t -> F t is an isomorphism (Lambek’s theorem for coalgebras):
F t ≅ tF t ≅ t
终端余代数通常在编程中被解释为生成(可能是无限的)数据结构或转换系统的方法。
A terminal coalgebra is usually interpreted in programming as a recipe for generating (possibly infinite) data structures or transition systems.
就像变形可用于评估初始代数一样,变形可用于共同评估终端余代数:
Just like a catamorphism can be used to evaluate an initial algebra, an anamorphism can be used to coevaluate a terminal coalgebra:
ana :: Functor f => (a -> f a) -> a -> Fix f
ana coalg = Fix . fmap (ana coalg) . coalgana :: Functor f => (a -> f a) -> a -> Fix f
ana coalg = Fix . fmap (ana coalg) . coalg
余代数的典型示例基于函子,其固定点是类型 元素的无限流e。这是函子:
A canonical example of a coalgebra is based on a functor whose fixed point is an infinite stream of elements of type e. This is the functor:
data StreamF e a = StreamF e a
deriving Functordata StreamF e a = StreamF e a
deriving Functor
这是它的不动点:
and this is its fixed point:
data Stream e = Stream e (Stream e)data Stream e = Stream e (Stream e)
的余代数StreamF e是一个函数,它采用类型的种子并生成由一个元素和下一个种子组成的a对(是对的一个奇特名称)。StreamF
A coalgebra for StreamF e is a function that takes the seed of type a and produces a pair (StreamF is a fancy name for a pair) consisting of an element and the next seed.
您可以轻松生成产生无限序列的余代数的简单示例,例如平方列表或倒数。
You can easily generate simple examples of coalgebras that produce infinite sequences, like the list of squares, or reciprocals.
一个更有趣的例子是产生素数列表的余代数。诀窍是使用无限列表作为载体。我们的起始种子将是列表[2..]。下一个种子将是该列表的尾部,删除所有 2 的倍数。这是一个以 3 开头的奇数列表。在下一步中,我们将取出该列表的尾部并删除所有 3 的倍数,依此类推。您可能认识埃拉托色尼筛子的构成。该余代数由以下函数实现:
A more interesting example is a coalgebra that produces a list of primes. The trick is to use an infinite list as a carrier. Our starting seed will be the list [2..]. The next seed will be the tail of this list with all multiples of 2 removed. It’s a list of odd numbers starting with 3. In the next step, we’ll take the tail of this list and remove all multiples of 3, and so on. You might recognize the makings of the sieve of Eratosthenes. This coalgebra is implemented by the following function:
era :: [Int] -> StreamF Int [Int]
era (p : ns) = StreamF p (filter (notdiv p) ns)
where notdiv p n = n `mod` p /= 0era :: [Int] -> StreamF Int [Int]
era (p : ns) = StreamF p (filter (notdiv p) ns)
where notdiv p n = n `mod` p /= 0
该余代数的变形生成素数列表:
The anamorphism for this coalgebra generates the list of primes:
primes = ana era [2..]primes = ana era [2..]
流是一个无限列表,因此应该可以将其转换为 Haskell 列表。为此,我们可以使用相同的函子StreamF来形成代数,并且可以对其进行变形。例如,这是将流转换为列表的变形:
A stream is an infinite list, so it should be possible to convert it to a Haskell list. To do that, we can use the same functor StreamF to form an algebra, and we can run a catamorphism over it. For instance, this is a catamorphism that converts a stream to a list:
toListC :: Fix (StreamF e) -> [e]
toListC = cata al
where al :: StreamF e [e] -> [e]
al (StreamF e a) = e : atoListC :: Fix (StreamF e) -> [e]
toListC = cata al
where al :: StreamF e [e] -> [e]
al (StreamF e a) = e : a
这里,同一固定点同时是同一内函子的初始代数和终端余代数。在任意范畴中,情况并不总是这样。一般来说,endofunctor 可能有许多(或没有)固定点。初始代数就是所谓的最小不动点,而末端余代数就是最大不动点。然而,在 Haskell 中,两者都是由相同的公式定义的,并且它们是一致的。
Here, the same fixed point is simultaneously an initial algebra and a terminal coalgebra for the same endofunctor. It’s not always like this, in an arbitrary category. In general, an endofunctor may have many (or no) fixed points. The initial algebra is the so called least fixed point, and the terminal coalgebra is the greatest fixed point. In Haskell, though, both are defined by the same formula, and they coincide.
列表的变形称为展开。为了创建有限列表,修改函子以生成一Maybe对:
The anamorphism for lists is called unfold. To create finite lists, the functor is modified to produce a Maybe pair:
unfoldr :: (b -> Maybe (a, b)) -> b -> [a]unfoldr :: (b -> Maybe (a, b)) -> b -> [a]
的值Nothing将终止列表的生成。
The value of Nothing will terminate the generation of the list.
余代数的一个有趣的例子与透镜有关。一个透镜可以表示为一对 getter 和 setter:
An interesting case of a coalgebra is related to lenses. A lens can be represented as a pair of a getter and a setter:
set :: a -> s -> a
get :: a -> sset :: a -> s -> a
get :: a -> s
这里,a通常是一些带有类型字段的产品数据类型s。getter 检索该字段的值,setter 则用新值替换该字段。这两个函数可以合二为一:
Here, a is usually some product data type with a field of type s. The getter retrieves the value of that field and the setter replaces this field with a new value. These two functions can be combined into one:
a -> (s, s -> a)a -> (s, s -> a)
我们可以进一步重写这个函数:
We can rewrite this function further as:
a -> Store s aa -> Store s a
我们定义了一个函子:
where we have defined a functor:
data Store s a = Store (s -> a) sdata Store s a = Store (s -> a) s
请注意,这不是一个由乘积之和构造的简单代数函子。它涉及到一个指数as。
Notice that this is not a simple algebraic functor constructed from sums of products. It involves an exponential as.
透镜是具有载体类型的函子的余代数a。我们之前已经看到这Store s也是一个共生体。事实证明,一个表现良好的透镜对应于一个与 comonad 结构兼容的余代数。我们将在下一节讨论这个问题。
A lens is a coalgebra for this functor with the carrier type a. We’ve seen before that Store s is also a comonad. It turns out that a well-behaved lens corresponds to a coalgebra that is compatible with the comonad structure. We’ll talk about this in the next section.
x。例如,4x2-1将表示为 (从零次方开始) [-1, 0, 4]。x. For instance, 4x2-1 would be represented as (starting with the zero’th power) [-1, 0, 4].x2y-3y3z。x2y-3y3z.unfoldr生成第一个素数的列表n。unfoldr to generate a list of the first n primes.如果我们将内函子解释为定义表达式的方式,那么代数让我们评估它们,而单子让我们形成和操纵它们。通过将代数与单子相结合,我们不仅获得了很多功能,而且还可以回答一些有趣的问题。其中一个问题涉及单子和附加词之间的关系。正如我们所看到的,每个附加词都定义了一个 monad(和一个 comonad)。问题是:每个单子(comonad)都可以从附加词导出吗?答案是肯定的。有一个完整的附加词家族可以生成一个给定的单子。我将向您展示两个这样的附加项。
If we interpret endofunctors as ways of defining expressions, algebras let us evaluate them and monads let us form and manipulate them. By combining algebras with monads we not only gain a lot of functionality but we can also answer a few interesting questions. One such question concerns the relation between monads and adjunctions. As we’ve seen, every adjunction defines a monad (and a comonad). The question is: Can every monad (comonad) be derived from an adjunction? The answer is positive. There is a whole family of adjunctions that generate a given monad. I’ll show you two such adjunction.
让我们回顾一下定义。单子是一个内函子m,具有两个满足某些相干条件的自然变换。这些转换的组成部分a是:
Let’s review the definitions. A monad is an endofunctor m equipped with two natural transformations that satisfy some coherence conditions. The components of these transformations at a are:
ηa :: a -> m a
μa :: m (m a) -> m aηa :: a -> m a
μa :: m (m a) -> m a
a同一内函子的代数是对特定对象(载体)以及态射的选择:
An algebra for the same endofunctor is a selection of a particular object — the carrier a — together with the morphism:
alg :: m a -> aalg :: m a -> a
首先要注意的是代数与 的方向相反ηa。直觉是ηa从 type 的值创建一个简单的表达式a。使代数与单子兼容的第一个相干条件确保使用载体为的代数计算该表达式可以a返回原始值:
The first thing to notice is that the algebra goes in the opposite direction to ηa. The intuition is that ηa creates a trivial expression from a value of type a. The first coherence condition that makes the algebra compatible with the monad ensures that evaluating this expression using the algebra whose carrier is a gives us back the original value:
alg ∘ ηa = idaalg ∘ ηa = ida
第二个条件源于以下事实:有两种计算双重嵌套表达式的方法m (m a)。我们可以先应用μa对表达式进行展平,然后使用代数的求值器;或者我们可以应用提升的求值器来计算内部表达式,然后将求值器应用于结果。我们希望这两种策略是等效的:
The second condition arises from the fact that there are two ways of evaluating the doubly nested expression m (m a). We can first apply μa to flatten the expression, and then use the evaluator of the algebra; or we can apply the lifted evaluator to evaluate the inner expressions, and then apply the evaluator to the result. We’d like the two strategies to be equivalent:
alg ∘ μa = alg ∘ m algalg ∘ μa = alg ∘ m alg
这里,是使用函子m alg进行提升所产生的态射。下面的通勤图描述了这两种情况(我替换为 ,以预测接下来的情况):algmmT
Here, m alg is the morphism resulting from lifting alg using the functor m. The following commuting diagrams describe the two conditions (I replaced m with T in anticipation of what follows):
我们还可以用 Haskell 表达这些条件:
We can also express these condition in Haskell:
alg . return = id
alg . join = alg . fmap algalg . return = id
alg . join = alg . fmap alg
让我们看一个小例子。列表 endofunctor 的代数由某种类型和一个从 列表a生成 的函数组成。我们可以通过选择元素类型和累加器类型都等于来表达这个函数:aafoldra
Let’s look at a small example. An algebra for a list endofunctor consists of some type a and a function that produces an a from a list of a. We can express this function using foldr by choosing both the element type and the accumulator type to be equal to a:
foldr :: (a -> a -> a) -> a -> [a] -> afoldr :: (a -> a -> a) -> a -> [a] -> a
f这个特定的代数由一个双参数函数(我们称之为 )和一个值指定z。列表函子恰好也是一个 monad,将return值转换为单例列表。代数的组成,这里foldr f z,之后return为x:
This particular algebra is specified by a two-argument function, let’s call it f, and a value z. The list functor happens to also be a monad, with return turning a value into a singleton list. The composition of the algebra, here foldr f z, after return takes x to:
foldr f z [x] = x `f` zfoldr f z [x] = x `f` z
其中 的操作f以中缀表示法编写。如果每个 都满足以下一致性条件,则代数与单子兼容x:
where the action of f is written in the infix notation. The algebra is compatible with the monad if the following coherence condition is satisfied for every x:
x `f` z = xx `f` z = x
如果我们将其视为f二元运算符,则此条件告诉我们这z是正确的单位。
If we look at f as a binary operator, this condition tells us that z is the right unit.
第二个一致性条件对列表的列表进行操作。的操作join连接各个列表。然后我们可以折叠结果列表。另一方面,我们可以先折叠各个列表,然后折叠结果列表。同样,如果我们将其解释f为二元运算符,则此条件告诉我们该二元运算是结合的。(a, f, z)当是幺半群时,这些条件肯定满足。
The second coherence condition operates on a list of lists. The action of join concatenates the individual lists. We can then fold the resulting list. On the other hand, we can first fold the individual lists, and then fold the resulting list. Again, if we interpret f as a binary operator, this condition tells us that this binary operation is associative. These conditions are certainly fulfilled when (a, f, z) is a monoid.
由于数学家更喜欢将其单子称为 monad T,因此他们将与其兼容的代数称为 T 代数。范畴C中给定单子 T 的 T 代数形成称为 Eilenberg-Moore 范畴的范畴,通常表示为 C T。该范畴中的态射是代数的同态。这些同态与我们在 F 代数中定义的同态相同。
Since mathematicians prefer to call their monads T, they call algebras compatible with them T-algebras. T-algebras for a given monad T in a category C form a category called the Eilenberg-Moore category, often denoted by CT. Morphisms in that category are homomorphisms of algebras. These are the same homomorphisms we’ve seen defined for F-algebras.
T 代数是由载体对象和求值器组成的对,(a, f)。UT从 C T到 C存在一个明显的健忘函子,它映射(a, f)到a。它还将 T 代数的同态映射到 C 中载体对象之间的相应态射。您可能还记得我们对附加物的讨论,健忘函子的左伴随称为自由函子。
A T-algebra is a pair consisting of a carrier object and an evaluator, (a, f). There is an obvious forgetful functor UT from CT to C, which maps (a, f) to a. It also maps a homomorphism of T-algebras to a corresponding morphism between carrier objects in C. You may remember from our discussion of adjunctions that the left adjoint to a forgetful functor is called a free functor.
的左伴随UT称为FT。它将 C 中的对象映射到 C Ta中的自由代数。这个自由代数的载体是。它的求值器是从back 到 的态射。由于是一个 monad,我们可以使用 monadic (Haskell ) 作为评估器。T aT (T a)T aTμajoin
The left adjoint to UT is called FT. It maps an object a in C to a free algebra in CT. The carrier of this free algebra is T a. Its evaluator is a morphism from T (T a) back to T a. Since T is a monad, we can use the monadic μa (Haskell join) as the evaluator.
我们仍然需要证明这是一个 T 代数。为此,必须满足两个一致性条件:
We still have to show that this is a T-algebra. For that, two coherence conditions must be satisified:
alg ∘ ηTa = idTaalg ∘ ηTa = idTa
alg ∘ μa = alg ∘ T algalg ∘ μa = alg ∘ T alg
但如果你代入代数的话,这些只是一元法则μ。
But these are just monadic laws, if you plug in μ for the algebra.
您可能还记得,每个附加词都定义了一个单子。事实证明,FT 和 U T 之间的连接定义了用于T构建 Eilenberg-Moore 范畴的单子。由于我们可以对每个单子执行此构造,因此我们得出结论,每个单子都可以从附加项生成。稍后我将向您展示还有另一个生成相同单子的附加项。
As you may recall, every adjunction defines a monad. It turns out that the adjunction between FT and UT defines the very monad T that was used in the construction of the Eilenberg-Moore category. Since we can perform this construction for every monad, we conclude that every monad can be generated from an adjunction. Later I’ll show you that there is another adjunction that generates the same monad.
计划如下:首先,我将向您展示这FT确实是 的左伴随UT。我将通过定义该附加的单位和单位并证明满足相应的三角形恒等式来完成此操作。然后我会告诉你这个附加生成的 monad 确实是我们原来的 monad。
Here’s the plan: First I’ll show you that FT is indeed the left adjoint of UT. I’ll do it by defining the unit and the counit of this adjunction and proving that the corresponding triangular identities are satisfied. Then I’ll show you that the monad generated by this adjunction is indeed our original monad.
附加的单位是自然变换:
The unit of the adjunction is the natural transformation:
η :: I -> UT ∘ FTη :: I -> UT ∘ FT
让我们计算一下a这个变换的分量。恒等函子给了我们a. 自由函子产生自由代数(T a, μa),而健忘函子将其简化为T a。a总而言之,我们得到了从到 的映射T a。我们将简单地使用 monad 的单位T作为该附加词的单位。
Let’s calculate the a component of this transformation. The identity functor gives us a. The free functor produces the free algebra (T a, μa), and the forgetful functor reduces it to T a. Altogether we get a mapping from a to T a. We’ll simply use the unit of the monad T as the unit of this adjunction.
我们看一下单位:
Let’s look at the counit:
ε :: FT ∘ UT -> Iε :: FT ∘ UT -> I
让我们计算它在某个 T 代数上的分量(a, f)。健忘的函子会忘记f,而自由的函子会生成对(T a, μa)。因此,为了定义 处的数的分量ε,(a, f)我们需要 Eilenberg-Moore 范畴中正确的态射,或者 T 代数的同态:
Let’s calculate its component at some T-algebra (a, f). The forgetful functor forgets the f, and the free functor produces the pair (T a, μa). So in order to define the component of the counit ε at (a, f), we need the right morphism in the Eilenberg-Moore category, or a homomorphism of T-algebras:
(T a, μa) -> (a, f)(T a, μa) -> (a, f)
这种同态应该将载体映射T a到a。让我们复活被遗忘的评估者吧f。这次我们将使用它作为 T 代数的同态。事实上,构成fT 代数的相同通勤图可以被重新解释以表明它是 T 代数的同态:
Such homomorphism should map the carrier T a to a. Let’s just resurrect the forgotten evaluator f. This time we’ll use it as a homomorphism of T-algebras. Indeed, the same commuting diagram that makes f a T-algebra may be re-interpreted to show that it’s a homomorphism of T-algebras:
因此,我们将(T-代数范畴中的一个对象)的ε单位自然变换的分量定义为。(a, f)f
We have thus defined the component of the counit natural transformation ε at (a, f) (an object in the category of T-algebras) to be f.
为了完成附加,我们还需要证明单位和单位满足三角恒等式。这些都是:
To complete the adjunction we also need to show that the unit and the counit satisfy triangular identites. These are:
第一个因单子的单位定律而成立T。第二个就是T代数定律(a, f)。
The first one holds because of the unit law for the monad T. The second is just the law of the T-algebra (a, f).
我们已经确定这两个函子形成了一个附属词:
We have established that the two functors form an adjunction:
FT ⊣ UTFT ⊣ UT
每个附加物都会产生一个单子。往返行程
Every adjunction gives rise to a monad. The round trip
UT ∘ FTUT ∘ FT
是 C 中产生相应单子的内函子。让我们看看它对对象的作用a是什么。创建的自由代数FT是(T a, μa). 健忘的函子FT会丢弃求值器。所以,确实,我们有:
is the endofunctor in C that gives rise to the corresponding monad. Let’s see what its action on an object a is. The free algebra created by FT is (T a, μa). The forgetful functor FT drops the evaluator. So, indeed, we have:
UT ∘ FT = TUT ∘ FT = T
正如预期的那样,附加物的单位是 monad 的单位T。
As expected, the unit of the adjunction is the unit of the monad T.
您可能还记得加法的计数通过以下公式产生一元乘法:
You may remember that the counint of the adjunction produces monadic muliplication through the following formula:
μ = R ∘ ε ∘ Lμ = R ∘ ε ∘ L
这是三个自然变换的水平组合,其中两个是恒等自然变换,分别映射L到L和R到R。中间的那个,即计数单位,是一种自然变换,其代数分量(a, f)是f。
This is a horizontal composition of three natural transformations, two of them being identity natural transformations mapping, respectively, L to L and R to R. The one in the middle, the counit, is a natural transformation whose component at an algebra (a, f) is f.
我们来计算一下分量μa。我们首先水平组合εafter ,这会产生atFT的分量。由于采用代数并选择评估器,我们最终得到。左边的水平组合不会改变任何东西,因为态射的作用是微不足道的。因此,确实,从附加项获得的 与原始 monad 的相同。εFTaFTa(T a, μa)εμaUTUTμμT
Let’s calculate the component μa. We first horizontally compose ε after FT, which results in the component of ε at FTa. Since FT takes a to the algebra (T a, μa), and ε picks the evaluator, we end up with μa. Horizontal composition on the left with UT doesn’t change anything, since the action of UT on morphisms is trivial. So, indeed, the μ obtained from the adjunction is the same as the μ of the original monad T.
我们之前已经见过 Kleisli 范畴。它是一个由另一个范畴C和一个 monad构造而成的范畴T。我们将这个范畴称为C T。克莱斯利范畴C T中的对象是C的对象,但态射不同。fKKleisli 范畴中从a到 的态射b对应于原始范畴中f从a到 的态射。我们称这种态射为从 到 的T b克莱斯利箭头。ab
We’ve seen the Kleisli category before. It’s a category constructed from another category C and a monad T. We’ll call this category CT. The objects in the Kleisli category CT are the objects of C, but the morphisms are different. A morphism fK from a to b in the Kleisli category corresponds to a morphism f from a to T b in the original category. We call this morphism a Kleisli arrow from a to b.
Kleisli 范畴中态射的复合是根据 Kleisli 箭头的单子复合来定义的。例如,让我们gK在之后撰写fK。在 Kleisli 范畴中,我们有:
Composition of morphisms in the Kleisli category is defined in terms of monadic composition of Kleisli arrows. For instance, let’s compose gK after fK. In the Kleisli category we have:
fK :: a -> b
gK :: b -> cfK :: a -> b
gK :: b -> c
在范畴C中,对应于:
which, in the category C, corresponds to:
f :: a -> T b
g :: b -> T cf :: a -> T b
g :: b -> T c
我们定义组成:
We define the composition:
hK = gK ∘ fKhK = gK ∘ fK
作为C 语言中的克莱斯利箭头
as a Kleisli arrow in C
h :: a -> T c
h = μ ∘ (T g) ∘ fh :: a -> T c
h = μ ∘ (T g) ∘ f
在 Haskell 中我们将其写为:
In Haskell we would write it as:
h = join . fmap g . fh = join . fmap g . f
有一个F从C到C T 的函子,它对对象的作用很简单。在吗啡上,它通过创建一个修饰 的返回值的 Kleisli 箭头,将Cf中的态射映射到C T中的态射。给定一个态射:f
There is a functor F from C to CT which acts trivially on objects. On morphims, it maps f in C to a morphism in CT by creating a Kleisli arrow that embellishes the return value of f. Given a morphism:
f :: a -> bf :: a -> b
它使用相应的 Kleisli 箭头在C T中创建一个态射:
it creates a morphism in CT with the corresponding Kleisli arrow:
η ∘ fη ∘ f
在 Haskell 中我们将其写为:
In Haskell we’d write it as:
return . freturn . f
我们还可以定义一个G从C T回到C 的函子。它从 Kleisli 范畴中获取一个对象并将其映射到C中的a对象。它对对应于 Kleisli 箭头的态射的作用:T afK
We can also define a functor G from CT back to C. It takes an object a from the Kleisli category and maps it to an object T a in C. Its action on a morphism fK corresponding to a Kleisli arrow:
f :: a -> T bf :: a -> T b
是C中的态射:
is a morphism in C:
T a -> T bT a -> T b
f通过先提升然后应用给出μ:
given by first lifting f and then applying μ:
μT b ∘ T fμT b ∘ T f
在 Haskell 表示法中,这将是:
In Haskell notation this would read:
G fT = join . fmap fG fT = join . fmap f
您可能会认为这是 monadic 绑定的定义join。
You may recognize this as the definition of monadic bind in terms of join.
很容易看出这两个函子形成了一个连接:
It’s easy to see that the two functors form an adjunction:
F ⊣ GF ⊣ G
他们的组合G ∘ F再现了原始的 monad T。
and their composition G ∘ F reproduces the original monad T.
所以这是产生相同单子的第二个附加词。事实上,有一整类附加词会在CAdj(C, T)上产生相同的单子。我们刚刚看到的 Kleisli 附属词是该范畴中的初始宾语,而 Eilenberg-Moore 附属词是终结宾语。T
So this is the second adjunction that produces the same monad. In fact there is a whole category of adjunctions Adj(C, T) that result in the same monad T on C. The Kleisli adjunction we’ve just seen is the initial object in this category, and the Eilenberg-Moore adjunction is the terminal object.
可以对任何comonad 进行类似的构造W。我们可以定义与共代数兼容的一类余代数。他们制作了以下图表:
Analogous constructions can be done for any comonad W. We can define a category of coalgebras that are compatible with a comonad. They make the following diagrams commute:
其中coa是其载体为 的余代数的共值态射a:
where coa is the coevaluation morphism of the coalgebra whose carrier is a:
coa :: a -> W acoa :: a -> W a
和是定义 comonad 的两个自然变换(在 Haskell 中,它们的ε组成δ部分称为extract和duplicate)。
and ε and δ are the two natural transformations defining the comonad (in Haskell, their components are called extract and duplicate).
UW从这些余代数的范畴到C,存在一个明显的健忘函子。它只是忘记了协同评估。我们将考虑它的右伴随FW。
There is an obvious forgetful functor UW from the category of these coalgebras to C. It just forgets the coevaluation. We’ll consider its right adjoint FW.
UW ⊣ FWUW ⊣ FW
健忘函子的右伴随称为 cofree 函子。FW生成 cofree 余代数。它将余代数分配给Ca中的一个对象。附加词将原始 comonad 再现为复合词。(W a, δa)FW ∘ UW
The right adjoint to a forgetful functor is called a cofree functor. FW generates cofree coalgebras. It assigns, to an object a in C, the coalgebra (W a, δa). The adjunction reproduces the original comonad as the composite FW ∘ UW.
类似地,我们可以用 co-Kleisli 箭头构造一个 co-Kleisli 范畴,并从相应的附加词重新生成 comonad。
Similarly, we can construct a co-Kleisli category with co-Kleisli arrows and regenerate the comonad from the corresponding adjunction.
让我们回到镜头的讨论上来。透镜可以写成余代数:
Let’s go back to our discussion of lenses. A lens can be written as a coalgebra:
coalgs :: a -> Store s acoalgs :: a -> Store s a
对于函子Store s:
for the functor Store s:
data Store s a = Store (s -> a) sdata Store s a = Store (s -> a) s
该余代数也可以表示为一对函数:
This coalgebra can be also expressed as a pair of functions:
set :: a -> s -> a
get :: a -> sset :: a -> s -> a
get :: a -> s
(将其视为a代表“全部”,并s作为其中的“小”部分。)就这一对而言,我们有:
(Think of a as standing for “all,” and s as a “small” part of it.) In terms of this pair, we have:
coalgs a = Store (set a) (get a)coalgs a = Store (set a) (get a)
这里,a是 类型的值a。请注意,部分应用set是一个函数s->a。
Here, a is a value of type a. Notice that partially applied set is a function s->a.
我们还知道这Store s是一个共同点:
We also know that Store s is a comonad:
instance Comonad (Store s) where
extract (Store f s) = f s
duplicate (Store f s) = Store (Store f) sinstance Comonad (Store s) where
extract (Store f s) = f s
duplicate (Store f s) = Store (Store f) s
问题是:在什么条件下透镜是这个共代数?第一个相干条件:
The question is: Under what conditions is a lens a coalgebra for this comonad? The first coherence condition:
εa ∘ coalg = idaεa ∘ coalg = ida
翻译为:
translates to:
set a (get a) = aset a (get a) = a
这是透镜定律,它表达了这样一个事实:如果将结构的字段设置a为其先前的值,则不会发生任何变化。
This is the lens law that expresses the fact that if you set a field of the structure a to its previous value, nothing changes.
第二个条件:
The second condition:
fmap coalg ∘ coalg = δa ∘ coalgfmap coalg ∘ coalg = δa ∘ coalg
需要更多的工作。fmap首先,回顾一下函子的定义Store:
requires a little more work. First, recall the definition of fmap for the Store functor:
fmap g (Store f s) = Store (g . f) sfmap g (Store f s) = Store (g . f) s
应用于fmap coalg结果给coalg我们:
Applying fmap coalg to the result of coalg gives us:
Store (coalg . set a) (get a)Store (coalg . set a) (get a)
另一方面,应用于产生duplicate的结果coalg:
On the other hand, applying duplicate to the result of coalg produces:
Store (Store (set a)) (get a)Store (Store (set a)) (get a)
为了使这两个表达式相等,下面的两个函数在作用Store于任意 时必须相等s:
For these two expressions to be equal, the two functions under Store must be equal when acting on an arbitrary s:
coalg (set a s) = Store (set a) scoalg (set a s) = Store (set a) s
展开coalg,我们得到:
Expanding coalg, we get:
Store (set (set a s)) (get (set a s)) = Store (set a) sStore (set (set a s)) (get (set a s)) = Store (set a) s
这相当于剩下的两个透镜定律。第一个:
This is equivalent to two remaining lens laws. The first one:
set (set a s) = set aset (set a s) = set a
告诉我们两次设置字段值与设置一次相同。第二定律:
tells us that setting the value of a field twice is the same as setting it once. The second law:
get (set a s) = sget (set a s) = s
告诉我们获取设置为的字段的值会s返回s。
tells us that getting a value of a field that was set to s gives s back.
换句话说,一个表现良好的透镜确实是函子的共代数Store。
In other words, a well-behaved lens is indeed a comonad coalgebra for the Store functor.
F :: C -> CT。提示:使用 monadic 的自然性条件μ。F :: C -> CT on morphisms. Hint: use the naturality condition for monadic μ.定义附加:
UW ⊣ FWDefine the adjunction:
UW ⊣ FW我要感谢 Gershom Bazerman 提供的有益评论。
I’d like to thank Gershom Bazerman for helpful comments.
我们可能对一个范畴中的态射有很多直觉,但我们都同意,如果一个对象到另一个对象存在态射,那么a这b两个对象在某种程度上是“相关的”。从某种意义上说,态射就是这种关系的证明。这在任何偏序集范畴中都是清晰可见的,其中态射是一种关系。一般来说,两个对象之间可能存在许多相同关系的“证据”。这些证明形成了一个集合,我们称之为 hom 集。当我们改变对象时,我们得到了从对象对到“证明”集的映射。这种映射是函数式的——第一个参数是逆变的,第二个参数是协变的。我们可以将其视为在范畴中的对象之间建立全局关系。这种关系由 hom 函子描述:
There are many intuitions that we may attach to morphisms in a category, but we can all agree that if there is a morphism from the object a to the object b than the two objects are in some way “related.” A morphism is, in a sense, the proof of this relation. This is clearly visible in any poset category, where a morphism is a relation. In general, there may be many “proofs” of the same relation between two objects. These proofs form a set that we call the hom-set. When we vary the objects, we get a mapping from pairs of objects to sets of “proofs.” This mapping is functorial — contravariant in the first argument and covariant in the second. We can look at it as establishing a global relationship between objects in the category. This relationship is described by the hom-functor:
C(-, =) :: Cop × C -> SetC(-, =) :: Cop × C -> Set
一般来说,任何像这样的函子都可以被解释为建立范畴中对象之间的关系。关系还可能涉及两个不同的范畴C和D。描述这种关系的函子具有以下签名,称为函子:
In general, any functor like this may be interpreted as establishing a relation between objects in a category. A relation may also involve two different categories C and D. A functor, which describes such a relation, has the following signature and is called a profunctor:
p :: Dop × C -> Setp :: Dop × C -> Set
数学家说它是一个从Cto 的函子D(注意反转),并使用斜线箭头作为它的符号:
Mathematicians say that it’s a profunctor from C to D (notice the inversion), and use a slashed arrow as a symbol for it:
C ↛ DC ↛ D
您可以将函子视为C的对象和D的对象之间的证明相关关系,其中集合的元素象征该关系的证明。每当为空时,和之间就没有关系。请记住,关系不必是对称的。p a bab
You may think of a profunctor as a proof-relevant relation between objects of C and objects of D, where the elements of the set symbolize proofs of the relation. Whenever p a b is empty, there is no relation between a and b. Keep in mind that relations don’t have to be symmetric.
另一个有用的直觉是 endofunctor 是容器这一想法的概括。然后,该类型的函子值可以被视为由类型 的元素作为键的 sp a b的容器。特别地, hom-profunctor 的元素是从到 的函数。baab
Another useful intuition is the generalization of the idea that an endofunctor is a container. A profunctor value of the type p a b could then be considered a container of bs that are keyed by elements of type a. In particular, an element of the hom-profunctor is a function from a to b.
在 Haskell 中,一个 profunctor 被定义为一个双参数类型构造函数,p配备了名为 的方法dimap,该构造函数提升一对函数,第一个函数朝着“错误”的方向运行:
In Haskell, a profunctor is defined as a two-argument type constructor p equipped with the method called dimap, which lifts a pair of functions, the first going in the “wrong” direction:
class Profunctor p where
dimap :: (c -> a) -> (b -> d) -> p a b -> p c dclass Profunctor p where
dimap :: (c -> a) -> (b -> d) -> p a b -> p c d
函子的函子性告诉我们,如果我们有一个与a相关的证明,那么只要有一个来自to和另一个来自to的态射,我们就得到与 相关的b证明。或者,我们可以将第一个函数视为将新密钥转换为旧密钥,第二个函数视为修改容器的内容。cdcabd
The functoriality of the profunctor tells us that if we have a proof that a is related to b, then we get the proof that c is related to d, as long as there is a morphism from c to a and another from b to d. Or, we can think of the first function as translating new keys to the old keys, and the second function as modifying the contents of the container.
对于在一个范畴内起作用的函子,我们可以从 类型的对角元素中提取相当多的信息p a a。只要我们有一对态射和,我们就可以证明 与b相关。更好的是,我们可以使用单个态射来达到非对角值。例如,如果我们有一个态射,我们可以将这对从 提升到:cb->aa->cf::a->b<f, idb>p b bp a b
For profunctors acting within one category, we can extract quite a lot of information from diagonal elements of the type p a a. We can prove that b is related to c as long as we have a pair of morphisms b->a and a->c. Even better, we can use a single morphism to reach off-diagonal values. For instance, if we have a morphism f::a->b, we can lift the pair <f, idb> to go from p b b to p a b:
dimap f id pbb :: p a bdimap f id pbb :: p a b
或者我们可以将这对<ida, f>从提升p a a到p a b:
Or we can lift the pair <ida, f> to go from p a a to p a b:
dimap id f paa :: p a bdimap id f paa :: p a b
由于函子是函子,我们可以用标准方式定义它们之间的自然变换。但在许多情况下,定义两个函子的对角元素之间的映射就足够了。这种变换称为自然变换,只要它满足反映我们将对角线元素连接到非对角线元素的两种方式的交换条件即可。p两个函子和之间的自然变换q(函子范畴 的成员)[Cop × C, Set]是态射族:
Since profunctors are functors, we can define natural transformations between them in the standard way. In many cases, though, it’s enough to define the mapping between diagonal elements of two profunctors. Such a transformation is called a dinatural transformation, provided it satisfies the commuting conditions that reflect the two ways we can connect diagonal elements to non-diagonal ones. A dinatural transformation between two profunctors p and q, which are members of the functor category [Cop × C, Set], is a family of morphisms:
αa :: p a a -> q a aαa :: p a a -> q a a
下图可以通勤,对于任何f::a->b:
for which the following diagram commutes, for any f::a->b:
请注意,这严格弱于自然性条件。如果α是 中的自然变换[Cop × C, Set],则上图可以由两个自然性平方和一个函子性条件(函子保留组合)构造而成q:
Notice that this is strictly weaker than the naturality condition. If α were a natural transformation in [Cop × C, Set], the above diagram could be constructed from two naturality squares and one functoriality condition (profunctor q preserving composition):
α请注意,中的自然变换的组成部分[Cop × C, Set]由一对对象进行索引α a b。另一方面,自然变换由一个对象索引,因为它仅映射相应函子的对角线元素。
Notice that a component of a natural transformation α in [Cop × C, Set] is indexed by a pair of objects α a b. A dinatural transformation, on the other hand, is indexed by one object, since it only maps diagonal elements of the respective profunctors.
我们现在准备从“代数”前进到范畴论的“微积分”。端(和共端)的演算借用了传统演算的思想甚至一些符号。特别地,coend可以被理解为无限和或积分,而end类似于无限乘积。甚至还有类似于狄拉克δ函数的东西。
We are now ready to advance from “algebra” to what could be considered the “calculus” of category theory. The calculus of ends (and coends) borrows ideas and even some notation from traditional calculus. In particular, the coend may be understood as an infinite sum or an integral, whereas the end is similar to an infinite product. There is even something that resembles the Dirac delta function.
终结是极限的概括,函子被函子取代。我们没有圆锥体,而是楔子。楔形的底是由函子 的对角线元素形成的p。楔形的顶点是一个对象(这里是一个集合,因为我们正在考虑集合值代函子),而边是将顶点映射到基部集合的一系列函数。您可能会将这个系列视为一个多态函数 - 一个其返回类型是多态的函数:
An end is a genaralization of a limit, with the functor replaced by a profunctor. Instead of a cone, we have a wedge. The base of a wedge is formed by diagonal elements of a profunctor p. The apex of the wedge is an object (here, a set, since we are considering Set-valued profunctors), and the sides are a family of functions mapping the apex to the sets in the base. You may think of this family as one polymorphic function — a function that’s polymorphic in its return type:
α :: forall a . apex -> p a aα :: forall a . apex -> p a a
与圆锥体不同,在楔形体中,我们没有任何连接底面顶点的函数。然而,正如我们之前所看到的,给定Cf::a->b中的任何态射,我们可以将和都连接到公共集合。因此,我们坚持下面的图可以通勤:p a ap b bp a b
Unlike in cones, within a wedge we don’t have any functions that would connect vertices of the base. However, as we’ve seen earlier, given any morphism f::a->b in C, we can connect both p a a and p b b to the common set p a b. We therefore insist that the following diagram commute:
这称为楔形条件。可以写成:
This is called the wedge condition. It can be written as:
p ida f ∘ αa = p f idb ∘ αbp ida f ∘ αa = p f idb ∘ αb
或者,使用 Haskell 表示法:
Or, using Haskell notation:
dimap id f . alpha = dimap f id . alphadimap id f . alpha = dimap f id . alpha
现在,我们可以继续进行通用构造,并将 的末端定义p为通用楔形 - 一组e与一系列函数一起的集合π,这样对于任何其他具有顶点a和家族的楔形α,都有一个独特的函数h::a->e使所有三角形都可交换:
We can now proceed with the universal construction and define the end of p as the uinversal wedge — a set e together with a family of functions π such that for any other wedge with the apex a and a family α there is a unique function h::a->e that makes all triangles commute:
πa ∘ h = αaπa ∘ h = αa
结尾符号为积分符号,下标位置为“积分变量”:
The symbol for the end is the integral sign, with the “integration variable” in the subscript position:
∫c p c c∫c p c c
的组成部分π称为末端投影图:
Components of π are called projection maps for the end:
πa :: ∫c p c c -> p a aπa :: ∫c p c c -> p a a
请注意,如果Cp是离散范畴(除了恒等式之外没有态射),则结束只是整个范畴C的所有对角线条目的全局乘积。稍后我将向您展示,在更一般的情况下,最终和该产品之间通过均衡器建立了关系。
Note that if C is a discrete category (no morphisms other than the identities) the end is just a global product of all diagonal entries of p across the whole category C. Later I’ll show you that, in the more general case, there is a relationship between the end and this product through an equalizer.
在 Haskell 中,最终公式直接转换为全称量词:
In Haskell, the end formula translates directly to the universal quantifier:
forall a. p a aforall a. p a a
严格来说,这只是 的所有对角元素的乘积p,但由于参数性,楔形条件会自动满足(我将在单独的博客文章中进行解释)。对于任何函数f :: a -> b,楔形条件为:
Strictly speaking, this is just a product of all diagonal elements of p, but the wedge condition is satisfied automatically due to parametricity (I’ll explain it in a separate blog post). For any function f :: a -> b, the wedge condition reads:
dimap f id . pi = dimap id f . pidimap f id . pi = dimap id f . pi
或者,使用类型注释:
or, with type annotations:
dimap f idb . pib = dimap ida f . piadimap f idb . pib = dimap ida f . pia
其中方程两边的类型为:
where both sides of the equation have the type:
Profunctor p => (forall c. p c c) -> p a bProfunctor p => (forall c. p c c) -> p a b
是pi多态投影:
and pi is the polymorphic projection:
pi :: Profunctor p => forall c. (forall a. p a a) -> p c c
pi e = epi :: Profunctor p => forall c. (forall a. p a a) -> p c c
pi e = e
在这里,类型推断会自动选择 的正确组件e。
Here, type inference automatically picks the right component of e.
正如我们能够将锥体的整套交换条件表示为一个自然变换一样,同样,我们可以将所有楔形条件分组为一个自然变换。为此,我们需要将常函子泛化Δc为常量函子,将所有对象对映射到单个对象c,并将所有态射对映射到该对象的恒等态射。楔子是从函子到函子的自然变换p。事实上,当我们意识到将Δc所有态射提升到一个恒等函数时,双自然六边形就会缩小为楔形菱形。
Just as we were able to express the whole set of commutation conditions for a cone as one natural transformation, likewise we can group all the wedge conditions into one dinatural transformation. For that we need the generalization of the constant functor Δc to a constant profunctor that maps all pairs of objects to a single object c, and all pairs of morphisms to the identity morphism for this object. A wedge is a dinatural transformation from that functor to the profunctor p. Indeed, the dinaturality hexagon shrinks down to the wedge diamond when we realize that Δc lifts all morphisms to one identity function.
还可以为Set以外的目标范畴定义结尾,但这里我们只考虑Set值的函子及其结尾。
Ends can also be defined for target categories other than Set, but here we’ll only consider Set-valued profunctors and their ends.
末端定义中的换相条件可以用均衡器来写。首先,让我们定义两个函数(我使用 Haskell 表示法,因为在这种情况下数学表示法似乎不太用户友好)。这些函数对应于楔形条件的两个收敛分支:
The commutation condition in the definition of the end can be written using an equalizer. First, let’s define two functions (I’m using Haskell notation, because mathematical notation seems to be less user-friendly in this case). These functions correspond to the two converging branches of the wedge condition:
lambda :: Profunctor p => p a a -> (a -> b) -> p a b
lambda paa f = dimap id f paa
rho :: Profunctor p => p b b -> (a -> b) -> p a b
rho pbb f = dimap f id pbblambda :: Profunctor p => p a a -> (a -> b) -> p a b
lambda paa f = dimap id f paa
rho :: Profunctor p => p b b -> (a -> b) -> p a b
rho pbb f = dimap f id pbb
这两个函数都将仿函子的对角线元素映射p到以下类型的多态函数:
Both functions map diagonal elements of the profunctor p to polymorphic functions of the type:
type ProdP p = forall a b. (a -> b) -> p a btype ProdP p = forall a b. (a -> b) -> p a b
这些函数有不同的类型。然而,如果我们形成一个大的产品类型,将 的所有对角线元素聚集在一起,我们可以统一它们的类型p:
These functions have different types. However, we can unify their types, if we form one big product type, gathering together all diagonal elements of p:
newtype DiaProd p = DiaProd (forall a. p a a)newtype DiaProd p = DiaProd (forall a. p a a)
函数lambda并rho从该产品类型中导出两个映射:
The functions lambda and rho induce two mappings from this product type:
lambdaP :: Profunctor p => DiaProd p -> ProdP p
lambdaP (DiaProd paa) = lambda paa
rhoP :: Profunctor p => DiaProd p -> ProdP p
rhoP (DiaProd paa) = rho paalambdaP :: Profunctor p => DiaProd p -> ProdP p
lambdaP (DiaProd paa) = lambda paa
rhoP :: Profunctor p => DiaProd p -> ProdP p
rhoP (DiaProd paa) = rho paa
最后p就是这两个函数的均衡器。请记住,均衡器选择两个函数相等的最大子集。在这种情况下,它选择楔形图交换的所有对角线元素的乘积的子集。
The end of p is the equalizer of these two functions. Remember that the equalizer picks the largest subset on which two functions are equal. In this case it picks the subset of the product of all diagonal elements for which the wedge diagrams commute.
结束的最重要的例子是一组自然变换。两个函子之间的自然变换F是G从 形式的 hom-sets 中选取的态射族C(F a, G a)。如果没有自然性条件,自然变换集将只是所有这些 hom 集的产物。事实上,在 Haskell 中,它是:
The most important example of an end is the set of natural transformations. A natural transformation between two functors F and G is a family of morphisms picked from hom-sets of the form C(F a, G a). If it weren’t for the naturality condition, the set of natural transformations would be just the product of all these hom-sets. In fact, in Haskell, it is:
forall a. f a -> g aforall a. f a -> g a
它在 Haskell 中起作用的原因是因为自然性源自参数化。然而,在 Haskell 之外,并非所有跨过此类 hom-set 的对角线部分都会产生自然变换。但请注意映射:
The reason it works in Haskell is because naturality follows from parametricity. Outside of Haskell, though, not all diagonal sections across such hom-sets will yield natural transformations. But notice that the mapping:
<a, b> -> C(F a, G b)<a, b> -> C(F a, G b)
是一个函子,所以研究它的结尾是有意义的。这是楔形条件:
is a profunctor, so it makes sense to study its end. This is the wedge condition:
让我们从集合中选择一个元素∫c C(F c, G c)。这两个投影会将这个元素映射到特定转换的两个组件,我们称它们为:
Let’s just pick one element from the set ∫c C(F c, G c). The two projections will map this element to two components of a particular transformation, let’s call them:
τa :: F a -> G a
τb :: F b -> G bτa :: F a -> G a
τb :: F b -> G b
在左分支中,我们<ida, G f>使用 hom 函子提升一对态射。您可能还记得,这种提升是通过同时进行预合成和后合成来实现的。当作用于被τa提升的对时,我们得到:
In the left branch, we lift a pair of morphisms <ida, G f> using the hom-functor. You may recall that such lifting is implemented as simultaneous pre- and post-composition. When acting on τa the lifted pair gives us:
G f ∘ τa ∘ idaG f ∘ τa ∘ ida
该图的另一个分支为我们提供了:
The other branch of the diagram gives us:
idb ∘ τb ∘ F fidb ∘ τb ∘ F f
楔子条件所要求的它们的相等性只不过是 的自然性条件τ。
Their equality, demanded by the wedge condition, is nothing but the naturality condition for τ.
正如预期的那样,对偶端称为共端。它是由称为“cowedge”(发音为“co wedge”,而不是“cow-edge”)的对偶楔形构成的。
As expected, the dual to an end is called a coend. It is constructed from a dual to a wedge called a cowedge (pronounced co-wedge, not cow-edge).
一头急躁的牛?
An edgy cow?
共尾的符号是积分符号,上标位置为“积分变量”:
The symbol for a coend is the integral sign with the “integration variable” in the superscript position:
∫ c p c c∫ c p c c
就像尾与乘积相关一样,余尾与余积或和相关(在这方面,它类似于积分,是和的极限)。我们没有投影,而是从余函子的对角线元素向下到共尾进行注入。如果没有牛边条件,我们可以说函子的同尾p要么是p a a, 要么p b b, 要么p c c,等等。或者我们可以说存在这样一个a,其共端就是集合p a a。我们在结束定义中使用的全称量词变成了共端的存在量词。
Just like the end is related to a product, the coend is related to a coproduct, or a sum (in this respect, it resembles an integral, which is a limit of a sum). Rather than having projections, we have injections going from the diagonal elements of the profunctor down to the coend. If it weren’t for the cowedge conditions, we could say that the coend of the profunctor p is either p a a, or p b b, or p c c, and so on. Or we could say that there exists such an a for which the coend is just the set p a a. The universal quantifier that we used in the definition of the end turns into an existential quantifier for the coend.
这就是为什么在伪 Haskell 中,我们将 coend 定义为:
This is why, in pseudo-Haskell, we would define the coend as:
exists a. p a aexists a. p a a
在 Haskell 中编码存在量词的标准方法是使用通用量化数据构造函数。因此我们可以定义:
The standard way of encoding existential quantifiers in Haskell is to use universally quantified data constructors. We can thus define:
data Coend p = forall a. Coend (p a a)data Coend p = forall a. Coend (p a a)
p a a这背后的逻辑是,无论a我们选择什么,都应该可以使用任何类型族的值构造一个共端。
The logic behind this is that it should be possible to construct a coend using a value of any of the family of types p a a, no matter what a we chose.
就像可以使用均衡器定义结束一样,可以使用 coequalizer 来描述共同结束。所有的边缘条件都可以通过p a b对所有可能的函数取一个巨大的余积来概括b->a。在 Haskell 中,这将表示为存在类型:
Just like an end can be defined using an equalizer, a coend can be described using a coequalizer. All the cowedge conditions can be summarized by taking one gigantic coproduct of p a b for all possible functions b->a. In Haskell, that would be expressed as an existential type:
data SumP p = forall a b. SumP (b -> a) (p a b)data SumP p = forall a b. SumP (b -> a) (p a b)
有两种求和类型的方法,通过使用提升函数dimap并将其应用到 profunctor p:
There are two ways of evaluating this sum type, by lifting the function using dimap and applying it to the profunctor p:
lambda, rho :: Profunctor p => SumP p -> DiagSum p
lambda (SumP f pab) = DiagSum (dimap f id pab)
rho (SumP f pab) = DiagSum (dimap id f pab)lambda, rho :: Profunctor p => SumP p -> DiagSum p
lambda (SumP f pab) = DiagSum (dimap f id pab)
rho (SumP f pab) = DiagSum (dimap id f pab)
其中DiagSum是 的对角元素之和p:
where DiagSum is the sum of diagonal elements of p:
data DiagSum p = forall a. DiagSum (p a a)data DiagSum p = forall a. DiagSum (p a a)
这两个函数的协同均衡器是 coend。通过识别通过将或应用于相同参数DiagSum p而获得的值来获得辅助均衡器。这里,参数是由函数和 的元素组成的对。和的应用会产生两个可能不同的类型值。在 coend 中,这两个值被识别,使得牛边条件自动满足。lambdarhob->ap a blambdarhoDiagSum p
The coequalizer of these two functions is the coend. A coequilizer is obtained from DiagSum p by identifying values that are obtained by applying lambda or rho to the same argument. Here, the argument is a pair consisting of a function b->a and an element of p a b. The application of lambda and rho produces two potentially different values of the type DiagSum p. In the coend, these two values are identified, making the cowedge condition automatically satisfied.
识别集合中相关元素的过程正式称为求商。为了定义商,我们需要一个等价关系 ~,一个自反、对称和传递的关系:
The process of identification of related elements in a set is formally known as taking a quotient. To define a quotient we need an equivalence relation ~, a relation that is reflexive, symmetric, and transitive:
a ~ a
if a ~ b then b ~ a
if a ~ b and b ~ c then a ~ ca ~ a
if a ~ b then b ~ a
if a ~ b and b ~ c then a ~ c
这种关系将集合分成等价类。每个类都由彼此相关的元素组成。我们通过从每个范畴中挑选一个代表来形成一个商集。一个典型的例子是将有理数定义为具有以下等价关系的整数对:
Such a relation splits the set into equivalence classes. Each class consists of elements that are related to each other. We form a quotient set by picking one representative from each class. A classic example is the definition of rational numbers as pairs of whole numbers with the following equivalence relation:
(a, b) ~ (c, d) iff a * d = b * c(a, b) ~ (c, d) iff a * d = b * c
很容易检查这是一个等价关系。一对(a, b)被解释为一个分数a/b,并且具有公约数的分数被识别。有理数是此类分数的等价类。
It’s easy to check that this is an equivalence relation. A pair (a, b) is interpreted as a fraction a/b, and fractions that have a common divisor are identified. A rational number is an equivalence class of such fractions.
您可能还记得我们之前对极限和余极限的讨论,hom 函子是连续的,也就是说,它保留极限。对偶而言,逆变 hom 函子将余极限变成极限。这些性质可以推广到ends 和coends,它们分别是limits 和colimits 的推广。特别是,我们得到了一个非常有用的恒等式,可以将共端转换为端:
You might recall from our earlier discussion of limits and colimits that the hom-functor is continuous, that is, it preserves limits. Dually, the contravariant hom-functor turns colimits into limits. These properties can be generalized to ends and coends, which are a generalization of limits and colimits, respectively. In particular, we get a very useful identity for converting coends to ends:
Set(∫ x p x x, c) ≅ ∫x Set(p x x, c)Set(∫ x p x x, c) ≅ ∫x Set(p x x, c)
让我们用伪 Haskell 来看看:
Let’s have a look at it in pseudo-Haskell:
(exists x. p x x) -> c ≅ forall x. p x x -> c(exists x. p x x) -> c ≅ forall x. p x x -> c
它告诉我们,采用存在类型的函数相当于多态函数。这是完全有道理的,因为这样的函数必须准备好处理可能在存在类型中编码的任何一种类型。同样的原则告诉我们,接受 sum 类型的函数必须作为 case 语句实现,并带有一组处理程序,每个处理程序对应 sum 中存在的每种类型。在这里,sum 类型被 coend 取代,并且一系列处理程序成为 end 或多态函数。
It tells us that a function that takes an existential type is equivalent to a polymorphic function. This makes perfect sense, because such a function must be prepared to handle any one of the types that may be encoded in the existential type. It’s the same principle that tells us that a function that accepts a sum type must be implemented as a case statement, with a tuple of handlers, one for every type present in the sum. Here, the sum type is replaced by a coend, and a family of handlers becomes an end, or a polymorphic function.
米田引理中出现的自然变换集可以使用结尾进行编码,从而得到以下公式:
The set of natural transformations that appears in the Yoneda lemma may be encoded using an end, resulting in the following formulation:
∫z Set(C(a, z), F z) ≅ F a∫z Set(C(a, z), F z) ≅ F a
还有一个对偶公式:
There is also a dual formula:
∫ z C(a, z) × F z ≅ F a∫ z C(a, z) × F z ≅ F a
这种恒等式很容易让人想起狄拉克δ函数的公式(一个函数δ(a - z),或者更确切地说是一个分布,在 处有一个无限峰值a = z)。在这里,hom 函子扮演着 delta 函数的角色。
This identity is strongly reminiscent of the formula for the Dirac delta function (a function δ(a - z), or rather a distribution, that has an infinite peak at a = z). Here, the hom-functor plays the role of the delta function.
这两个恒等式有时被称为忍者米田引理。
Together these two identities are sometimes called the Ninja Yoneda lemma.
为了证明第二个公式,我们将使用米田嵌入的结果,该结果表明,当且仅当两个对象的 hom 函子同构时,两个对象才是同构的。换句话说a ≅ b,当且仅当存在类型的自然转换:
To prove the second formula, we will use the consequence of the Yoneda embedding, which states that two objects are isomorphic if and only if their hom-functors are isomorphic. In other words a ≅ b if and only if there is a natural transformation of the type:
[C, Set](C(a, -), C(b, =))[C, Set](C(a, -), C(b, =))
这是同构。
that is an isomorphism.
我们首先将要证明的恒等式的左侧插入到某个任意对象的 hom 函子中c:
We start by inserting the left-hand side of the identity we want to prove inside a hom-functor that’s going to some arbitrary object c:
Set(∫ z C(a, z) × F z, c)Set(∫ z C(a, z) × F z, c)
使用连续性参数,我们可以将 coend 替换为 end:
Using the continuity argument, we can replace the coend with the end:
∫z Set(C(a, z) × F z, c)∫z Set(C(a, z) × F z, c)
我们现在可以利用乘积和指数之间的附加:
We can now take advantage of the adjunction between the product and the exponential:
∫z Set(C(a, z), c(F z))∫z Set(C(a, z), c(F z))
我们可以利用米田引理“进行积分”得到:
We can “perform the integration” by using the Yoneda lemma to get:
c(F a)c(F a)
这个指数对象与 hom 集同构:
This exponential object is isomorphic to the hom-set:
Set(F a, c)Set(F a, c)
最后,我们利用米田嵌入来实现同构:
Finally, we take advantage of the Yoneda embedding to arrive at the isomorphism:
∫ z C(a, z) × F z ≅ F a∫ z C(a, z) × F z ≅ F a
让我们进一步探讨这样的想法,即函子描述了一种关系——更准确地说,是一种与证明相关的关系,这意味着该集合表示与 相关的p a b证明集合。如果我们有两个关系,我们可以尝试组合它们。如果存在一个中间对象使得和都非空,我们会说这与after的组合有关。这种新关系的证明都是个体关系的成对证明。因此,在理解存在量词对应于共尾,并且两个集合的笛卡尔积对应于“证明对”的情况下,我们可以使用以下公式来定义函子的组合:abpqabqpcq b cp c a
Let’s explore further the idea that a profunctor describes a relation — more precisely, a proof-relevant relation, meaning that the set p a b represents the set of proofs that a is related to b. If we have two relations p and q we can try to compose them. We’ll say that a is related to b through the composition of q after p if there exist an intermediary object c such that both q b c and p c a are non-empty. The proofs of this new relation are all pairs of proofs of individual relations. Therefore, with the understanding that the existential quantifier corresponds to a coend, and the cartesian product of two sets corresponds to “pairs of proofs,” we can define composition of profunctors using the following formula:
(q ∘ p) a b = ∫ c p c a × q b c(q ∘ p) a b = ∫ c p c a × q b c
Data.Profunctor.Composition经过一些重命名后,这是等效的 Haskell 定义:
Here’s the equivalent Haskell definition from Data.Profunctor.Composition, after some renaming:
data Procompose q p a b where
Procompose :: q a c -> p c b -> Procompose q p a bdata Procompose q p a b where
Procompose :: q a c -> p c b -> Procompose q p a b
这是使用广义代数数据类型或 GADT 语法,其中自由类型变量(此处c)自动进行存在量化。因此, (未柯里化的)数据构造函数Procompose等价于:
This is using generalized algebraic data type, or GADT syntax, in which a free type variable (here c) is automatically existentially quanitified. The (uncurried) data constructor Procompose is thus equivalent to:
exists c. (q a c, p c b)exists c. (q a c, p c b)
如此定义的组合单位是 hom 函子——这直接来自 Ninja Yoneda 引理。因此,询问是否存在一个函子充当态射的范畴是有意义的。答案是肯定的,但需要注意的是,函子组合的结合律和恒等律仅适用于自然同构。这样的范畴,其中定律在同构之前都是有效的,称为二范畴(比 2 范畴更一般)。所以我们有一个二范畴Prof,其中对象是范畴,态射是函子,态射之间的态射(又名双细胞)是自然变换。事实上,我们还可以走得更远,因为除了函子之外,我们还有正则函子作为范畴之间的态射。具有两种态射的范畴称为双范畴。
The unit of so defined composition is the hom-functor — this immediately follows from the Ninja Yoneda lemma. It makes sense, therefore, to ask the question if there is a category in which profunctors serve as morphisms. The answer is positive, with the caveat that both associativity and identity laws for profunctor composition hold only up to natural isomorphism. Such a category, where laws are valid up to isomorphism, is called a bicategory (which is more general than a 2-category). So we have a bicategory Prof, in which objects are categories, morphisms are profunctors, and morphisms between morphisms (a.k.a., two-cells) are natural transformations. In fact, one can go even further, because beside profunctors, we also have regular functors as morphisms between categories. A category which has two types of morphisms is called a double category.
Prounctor 在 Haskell 透镜库和箭头库中发挥着重要作用。
Profunctors play an important role in the Haskell lens library and in the arrow library.
到目前为止,我们主要处理单个范畴或一对范畴。在某些情况下,这有点太受限了。例如,在定义范畴C中的限制时,我们引入了索引范畴I作为模式的模板,该模式将构成锥体的基础。引入另一个范畴(一个琐碎的范畴)作为圆锥体顶点的模板是有意义的。相反,我们使用常量函子Δcfrom Ito C。
So far we’ve been mostly working with a single category or a pair of categories. In some cases that was a little too constraining. For instance, when defining a limit in a category C, we introduced an index category I as the template for the pattern that would form the basis for our cones. It would have made sense to introduce another category, a trivial one, to serve as a template for the apex of the cone. Instead we used the constant functor Δc from I to C.
是时候解决这个尴尬了。让我们使用三个范畴来定义限制。D让我们从索引范畴I到C 的函子开始。这是选择圆锥体底面的函子——图函子。
It’s time to fix this awkwardness. Let’s define a limit using three categories. Let’s start with the functor D from the index category I to C. This is the functor that selects the base of the cone — the diagram functor.
新添加的是范畴1,它包含单个对象(和单个恒等态射)。K从I到这一范畴,只有一个可能的函子。它将所有对象映射到1中的唯一对象,并将所有态射映射到恒等态射。F从1到C 的任何函子都会为我们的圆锥体选择一个潜在的顶点。
The new addition is the category 1 that contains a single object (and a single identity morphism). There is only one possible functor K from I to this category. It maps all objects to the only object in 1, and all morphisms to the identity morphism. Any functor F from 1 to C picks a potential apex for our cone.
ε圆锥体是从F ∘ K到 的自然变换D。请注意,它的F ∘ K作用与我们原来的完全相同Δc。下图显示了这种转变。
A cone is a natural transformation ε from F ∘ K to D. Notice that F ∘ K does exactly the same thing as our original Δc. The following diagram shows this transformation.
我们现在可以定义一个通用属性来选择“最佳”这样的函子F。这会将1F映射到C中的极限对象,并且从到 的自然变换将提供相应的投影。这个通用函子称为沿的右 Kan 扩展,并用 表示。DεF ∘ KDDKRanKD
We can now define a universal property that picks the “best” such functor F. This F will map 1 to the object that is the limit of D in C, and the natural transformation ε from F ∘ K to D will provide the corresponding projections. This universal functor is called the right Kan extension of D along K and is denoted by RanKD.
让我们制定通用属性。假设我们有另一个锥体——这是另一个函子以及从到 的F'自然变换。ε'F' ∘ KD
Let’s formulate the universal property. Suppose we have another cone — that is another functor F' together with a natural transformation ε' from F' ∘ K to D.
如果 Kan 扩展F = RanKD存在,则必须存在σ从F'到 它的唯一自然变换,以便ε'通过 因式分解ε,即:
If the Kan extension F = RanKD exists, there must be a unique natural transformation σ from F' to it, such that ε' factorizes through ε, that is:
ε' = ε . (σ ∘ K)ε' = ε . (σ ∘ K)
这里,σ ∘ K是两个自然变换的水平组合(其中之一是 上的恒等自然变换K)。然后,该变换与 垂直组合ε。
Here, σ ∘ K is the horizontal composition of two natural transformations (one of them being the identity natural transformation on K). This transformation is then vertically composed with ε.
在组件中,当作用于Ii中的对象时,我们得到:
In components, when acting on an object i in I, we get:
ε'i = εi ∘ σ K iε'i = εi ∘ σ K i
在我们的例子中,σ只有一个组件对应于单个对象1。所以,事实上,这是从 定义的圆锥顶点F'到 定义的通用圆锥顶点的唯一态射RanKD。通勤条件正是极限定义所要求的条件。
In our case, σ has only one component corresponding to the single object of 1. So, indeed, this is the unique morphism from the apex of the cone defined by F' to the apex of the universal cone defined by RanKD. The commuting conditions are exactly the ones required by the definition of a limit.
但是,重要的是,我们可以自由地将平凡范畴1替换为任意范畴A,并且正确 Kan 扩展的定义仍然有效。
But, importantly, we are free to replace the trivial category 1 with an arbitrary category A, and the definition of the right Kan extension remains valid.
D::I->C函子沿函子的右 Kan 延伸K::I->A是一个函子F::A->C(表示为RanKD)和一个自然变换
The right Kan extension of the functor D::I->C along the functor K::I->A is a functor F::A->C (denoted RanKD) together with a natural transformation
ε :: F ∘ K -> Dε :: F ∘ K -> D
这样对于任何其他函子F'::A->C和自然变换
such that for any other functor F'::A->C and a natural transformation
ε' :: F' ∘ K -> Dε' :: F' ∘ K -> D
有一种独特的自然转变
there is a unique natural transformation
σ :: F' -> Fσ :: F' -> F
因式分解ε':
that factorizes ε':
ε' = ε . (σ ∘ K)ε' = ε . (σ ∘ K)
这有点拗口,但可以在这张漂亮的图表中形象化:
This is quite a mouthful, but it can be visualized in this nice diagram:
看待这个问题的一个有趣的方式是注意到,在某种意义上,Kan 扩展的作用类似于“函子乘法”的逆。有些作者甚至使用 的D/K符号RanKD。事实上,在这种表示法中, 的定义ε(也称为右 Kan 扩展的计数单位)看起来像简单的消去:
An interesting way of looking at this is to notice that, in a sense, the Kan extension acts like the inverse of “functor multiplication.” Some authors go as far as use the notation D/K for RanKD. Indeed, in this notation, the definition of ε, which is also called the counit of the right Kan extension, looks like simple cancellation:
ε :: D/K ∘ K -> Dε :: D/K ∘ K -> D
Kan 扩展还有另一种解释。考虑函子K将范畴I嵌入到A中。在最简单的情况下,我可能只是A的子范畴。我们有一个D将I映射到C 的函子。我们可以扩展到在整个AD上定义的函子吗?理想情况下,这样的扩展将使组合同构于. 换句话说,将把 的域扩展到。但全面的同构通常要求太多,我们只需要一半即可完成,即从到 的单向自然变换。(左 Kan 扩展选择另一个方向。)FF ∘ KDFDAεF ∘ KD
There is another interpretation of Kan extensions. Consider that the functor K embeds the category I inside A. In the simplest case I could just be a subcategory of A. We have a functor D that maps I to C. Can we extend D to a functor F that is defined on the whole of A? Ideally, such an extension would make the composition F ∘ K be isomorphic to D. In other words, F would be extending the domain of D to A. But a full-blown isomorphism is usually too much to ask, and we can do with just half of it, namely a one-way natural transformation ε from F ∘ K to D. (The left Kan extension picks the other direction.)
当然,当函子K在对象上不单射或在 hom 集上不忠实时,嵌入图片就会崩溃,就像极限的例子一样。在这种情况下,Kan 扩展会尽力推断丢失的信息。
Of course, the embedding picture breaks down when the functor K is not injective on objects or not faithful on hom-sets, as in the example of the limit. In that case, the Kan extension tries its best to extrapolate the lost information.
现在假设对于任何D(和固定的K)都存在正确的 Kan 扩展。在这种情况下RanK -(用破折号替换)是从函子范畴到函子范畴的D函子。事实证明,这个函子是预组合函子的右伴随。后者将函子 in 映射到函子 in 。附加项是:[I, C][A, C]-∘K[A, C][I, C]
Now suppose that the right Kan extension exists for any D (and a fixed K). In that case RanK - (with the dash replacing D) is a functor from the functor category [I, C] to the functor category [A, C]. It turns out that this functor is the right adjoint to the precomposition functor -∘K. The latter maps functors in [A, C] to functors in [I, C]. The adjunction is:
[I, C](F' ∘ K, D) ≅ [A, C](F', RanKD)[I, C](F' ∘ K, D) ≅ [A, C](F', RanKD)
这只是对以下事实的重述:我们所说的每一个自然变换都ε'对应着一个我们称之为的独特的自然变换σ。
It is just a restatement of the fact that to every natural transformation we called ε' corresponds a unique natural transformation we called σ.
此外,如果我们选择范畴I与C相同,我们可以用恒等函子IC代替D。我们得到以下身份:
Furthermore, if we chose the category I to be the same as C, we can substitute the identity functor IC for D. We get the following identity:
[C, C](F' ∘ K, IC) ≅ [A, C](F', RanKIC)[C, C](F' ∘ K, IC) ≅ [A, C](F', RanKIC)
我们现在可以选择F'与 相同RanKIC。在这种情况下,右侧包含恒等自然变换,与之相对应,左侧给出以下自然变换:
We can now chose F' to be the same as RanKIC. In that case the right hand side contains the identity natural transformation and, corresponding to it, the left hand side gives us the following natural transformation:
ε :: RanKIC ∘ K -> ICε :: RanKIC ∘ K -> IC
这看起来非常像附加词的计数:
This looks very much like the counit of an adjunction:
RanKIC ⊣ KRanKIC ⊣ K
事实上,恒等函子沿函子的右 Kan 扩展K可用于计算 的左伴随K。为此,还需要一个条件:函子 必须保留正确的 Kan 扩展K。扩展的保留意味着,如果我们计算用 预组合的函子的 Kan 扩展K,我们应该得到与用 预组合原始 Kan 扩展相同的结果K。在我们的例子中,这个条件简化为:
Indeed, the right Kan extension of the identity functor along a functor K can be used to calculate the left adjoint of K. For that, one more condition is necessary: the right Kan extension must be preserved by the functor K. The preservation of the extension means that, if we calculate the Kan extension of the functor precomposed with K, we should get the same result as precomposing the original Kan extesion with K. In our case, this condition simplifies to:
K ∘ RanKIC ≅ RanKKK ∘ RanKIC ≅ RanKK
请注意,使用除 K 表示法,附加词可以写为:
Notice that, using the division-by-K notation, the adjunction can be written as:
I/K ⊣ KI/K ⊣ K
这证实了我们的直觉,即附加词描述了某种逆元。保存条件变为:
which confirms our intuition that an adjunction describes some kind of an inverse. The preservation condition becomes:
K ∘ I/K ≅ K/KK ∘ I/K ≅ K/K
函子沿其自身的右 Kan 延拓,K/K称为同密度单子。
The right Kan extension of a functor along itself, K/K, is called a codensity monad.
伴随公式是一个重要的结果,因为我们很快就会看到,我们可以使用端(共端)计算 Kan 扩展,从而为我们提供了找到右(和左)伴随的实用方法。
The adjunction formula is an important result because, as we’ll see soon, we can calculate Kan extensions using ends (coends), thus giving us practical means of finding right (and left) adjoints.
有一个对偶结构为我们提供了左 Kan 延伸。为了建立一些直觉,我们可以从 colimit 的定义开始,并重新构造它以使用单例范畴1。我们通过使用函子D::I->C形成其底部并使用函子F::1->C选择其顶点来构建一个圆锥。
There is a dual construction that gives us the left Kan extension. To build some intuition, we’ll can start with the definition of a colimit and restructure it to use the singleton category 1. We build a cocone by using the functor D::I->C to form its base, and the functor F::1->C to select its apex.
椰子的侧面,即注射,是η从D到 的自然转变的组成部分F ∘ K。
The sides of the cocone, the injections, are components of a natural transformation η from D to F ∘ K.
colimit 是通用的可果。因此对于任何其他函子F'和自然变换
The colimit is the universal cocone. So for any other functor F' and a natural transformation
η' :: D -> F'∘ Kη' :: D -> F'∘ K
σ从F到存在着独特的自然转变F'
there is a unique natural transformation σ from F to F'
这样:
such that:
η' = (σ ∘ K) . ηη' = (σ ∘ K) . η
如下图所示:
This is illustrated in the following diagram:
将单例范畴1替换为A,该定义自然地推广到左 Kan 扩展的定义,用 表示LanKD。
Replacing the singleton category 1 with A, this definition naturally generalized to the definition of the left Kan extension, denoted by LanKD.
自然变换:
The natural transformation:
η :: D -> LanKD ∘ Kη :: D -> LanKD ∘ K
称为左坎延伸的单位。
is called the unit of the left Kan extension.
和以前一样,我们可以重新构建自然变换之间的一一对应关系:
As before, we can recast the one-to-one correspondence between natural transformations:
η' = (σ ∘ K) . ηη' = (σ ∘ K) . η
就附加而言:
in terms of the adjunction:
[A, C](LanKD, F') ≅ [I, C](D, F' ∘ K)[A, C](LanKD, F') ≅ [I, C](D, F' ∘ K)
换句话说,左 Kan 扩展是后合成的左伴随,右 Kan 扩展是 的右伴随K。
In other words, the left Kan extension is the left adjoint, and the right Kan extension is the right adjoint of the postcomposition with K.
就像恒等函子的右 Kan 扩展可用于计算 的左伴随一样K,恒等函子的左 Kan 扩展结果是 的右伴随K(作为η伴随的单位):
Just like the right Kan extension of the identity functor could be used to calculate the left adjoint of K, the left Kan extension of the identity functor turns out to be the right adjoint of K (with η being the unit of the adjunction):
K ⊣ LanKICK ⊣ LanKIC
结合两个结果,我们得到:
Combining the two results, we get:
RanKIC ⊣ K ⊣ LanKICRanKIC ⊣ K ⊣ LanKIC
Kan 扩展的真正力量来自于这样一个事实:它们可以使用端(和共端)来计算。为简单起见,我们将注意力限制在目标范畴C为Set的情况,但公式可以扩展到任何范畴。
The real power of Kan extensions comes from the fact that they can be calculated using ends (and coends). For simplicity, we’ll restrict our attention to the case where the target category C is Set, but the formulas can be extended to any category.
让我们重新回顾一下 Kan 扩展可用于将函子的操作扩展到其原始域之外的想法。假设将IK嵌入到A中。Functor将I映射到Set。我们可以说,对于的图像中的任何对象,即扩展函子映射到。问题是,如何处理A中那些在 的图像之外的对象?这个想法是,每个这样的对象都可能通过大量态射与 图像中的每个对象相关联。函子必须保留这些态射。从对象到图像的态射的总体由 hom 函子来表征:DaKa = K iaD iKKaK
Let’s revisit the idea that a Kan extension can be used to extend the action of a functor outside of its original domain. Suppose that K embeds I inside A. Functor D maps I to Set. We could just say that for any object a in the image of K, that is a = K i, the extended functor maps a to D i. The problem is, what to do with those objects in A that are outside of the image of K? The idea is that every such object is potentially connected through lots of morphisms to every object in the image of K. A functor must preserve these morphisms. The totality of morphisms from an object a to the image of K is characterized by the hom-functor:
A(a, K -)A(a, K -)
请注意,这个 hom 函子是两个函子的组合:
Notice that this hom-functor is a composition of two functors:
A(a, K -) = A(a, -) ∘ KA(a, K -) = A(a, -) ∘ K
右 Kan 扩展是函子组合的右伴随:
The right Kan extension is the right adjoint of functor composition:
[I, Set](F' ∘ K, D) ≅ [A, Set](F', RanKD)[I, Set](F' ∘ K, D) ≅ [A, Set](F', RanKD)
F'让我们看看当我们用 hom 函子替换时会发生什么:
Let’s see what happens when we replace F' with the hom functor:
[I, Set](A(a, -) ∘ K, D) ≅ [A, Set](A(a, -), RanKD)[I, Set](A(a, -) ∘ K, D) ≅ [A, Set](A(a, -), RanKD)
然后内联组合:
and then inline the composition:
[I, Set](A(a, K -), D) ≅ [A, Set](A(a, -), RanKD)[I, Set](A(a, K -), D) ≅ [A, Set](A(a, -), RanKD)
右侧可以使用米田引理来简化:
The right hand side can be reduced using the Yoneda lemma:
[I, Set](A(a, K -), D) ≅ RanKD a[I, Set](A(a, K -), D) ≅ RanKD a
我们现在可以重写自然变换集作为结尾,以获得正确 Kan 扩展的非常方便的公式:
We can now rewrite the set of natural transformations as the end to get this very convenient formula for the right Kan extension:
RanKD a ≅ ∫i Set(A(a, K i), D i)RanKD a ≅ ∫i Set(A(a, K i), D i)
对于左 Kan 扩展,有一个用余数表示的类似公式:
There is an analogous formula for the left Kan extension in terms of a coend:
LanKD a = ∫i A(K i, a) × D iLanKD a = ∫i A(K i, a) × D i
为了证明这种情况,我们将证明这确实是函子组合的左伴随:
To see that this is the case, we’ll show that this is indeed the left adjoint to functor composition:
[A, Set](LanKD, F') ≅ [I, Set](D, F'∘ K)[A, Set](LanKD, F') ≅ [I, Set](D, F'∘ K)
让我们将公式代入左侧:
Let’s substitute our formula in the left hand side:
[A, Set](∫i A(K i, -) × D i, F')[A, Set](∫i A(K i, -) × D i, F')
这是一组自然变换,因此可以将其重写为结束:
This is a set of natural transformations, so it can be rewritten as an end:
∫a Set(∫i A(K i, a) × D i, F'a)∫a Set(∫i A(K i, a) × D i, F'a)
利用 hom 函子的连续性,我们可以将 coend 替换为 end:
Using the continuity of the hom-functor, we can replace the coend with the end:
∫a ∫i Set(A(K i, a) × D i, F'a)∫a ∫i Set(A(K i, a) × D i, F'a)
我们可以使用乘积指数附加:
We can use the product-exponential adjunction:
∫a ∫i Set(A(K i, a), (F'a)D i)∫a ∫i Set(A(K i, a), (F'a)D i)
指数与相应的 hom 集同构:
The exponential is isomorphic to the corresponding hom-set:
∫a ∫i Set(A(K i, a), A(D i, F'a))∫a ∫i Set(A(K i, a), A(D i, F'a))
有一个称为富比尼定理的定理,它允许我们交换两端:
There is a theorem called the Fubini theorem that allows us to swap the two ends:
∫i ∫a Set(A(K i, a), A(D i, F'a))∫i ∫a Set(A(K i, a), A(D i, F'a))
内端表示两个函子之间的自然变换集合,因此我们可以使用米田引理:
The inner end represents the set of natural transformations between two functors, so we can use the Yoneda lemma:
∫i A(D i, F'(K i))∫i A(D i, F'(K i))
这确实是一组自然变换,形成了我们要证明的附加条件的右侧:
This is indeed the set of natural transformations that forms the right hand side of the adjunction we set out to prove:
[I, Set](D, F'∘ K)[I, Set](D, F'∘ K)
这些使用端、余端和米田引理的计算对于端的“演算”来说是非常典型的。
These kinds of calculations using ends, coends, and the Yoneda lemma are pretty typical for the “calculus” of ends.
Kan 扩展的 end/coend 公式可以很容易地转换为 Haskell。让我们从正确的扩展开始:
The end/coend formulas for Kan extensions can be easily translated to Haskell. Let’s start with the right extension:
RanKD a ≅ ∫i Set(A(a, K i), D i)RanKD a ≅ ∫i Set(A(a, K i), D i)
我们用通用量词替换 end,用函数类型替换 hom-sets:
We replace the end with the universal quantifier, and hom-sets with function types:
newtype Ran k d a = Ran (forall i. (a -> k i) -> d i)newtype Ran k d a = Ran (forall i. (a -> k i) -> d i)
查看此定义,很明显Ran必须包含可以应用该函数的类型值,以及两个函子和a之间的自然转换。例如,假设是树函子,是列表函子,并且给你一个。如果你向它传递一个函数:kdkdRan Tree [] String
Looking at this definition, it’s clear that Ran must contain a value of type a to which the function can be applied, and a natural transformation between the two functors k and d. For instance, suppose that k is the tree functor, and d is the list functor, and you were given a Ran Tree [] String. If you pass it a function:
f :: String -> Tree Intf :: String -> Tree Int
你会得到一个列表Int,等等。正确的 Kan 扩展将使用您的函数生成一棵树,然后将其重新打包到一个列表中。例如,您可以向它传递一个解析器,该解析器从字符串生成解析树,并且您将获得与该树的深度优先遍历相对应的列表。
you’ll get back a list of Int, and so on. The right Kan extension will use your function to produce a tree and then repackage it into a list. For instance, you may pass it a parser that generates a parsing tree from a string, and you’ll get a list that corresponds to the depth-first traversal of this tree.
右 Kan 扩展可用于通过用恒等函子替换函子来计算给d定函子的左伴随。这导致函子的左伴随k由以下类型的多态函数集表示:
The right Kan extension can be used to calculate the left adjoint of a given functor by replacing the functor d with the identity functor. This leads to the left adjoint of a functor k being represented by the set of polymorphic functions of the type:
forall i. (a -> k i) -> iforall i. (a -> k i) -> i
假设这k是幺半群范畴中的健忘函子。然后全称量词遍历所有幺半群。当然,在 Haskell 中我们无法表达幺半群定律,但以下是所得自由函子的一个不错的近似(健忘函子k是对象上的恒等式):
Suppose that k is the forgetful functor from the category of monoids. The universal quantifier then goes over all monoids. Of course, in Haskell we cannot express monoidal laws, but the following is a decent approximation of the resulting free functor (the forgetful functor k is an identity on objects):
type Lst a = forall i. Monoid i => (a -> i) -> itype Lst a = forall i. Monoid i => (a -> i) -> i
正如预期的那样,它生成免费的幺半群,或 Haskell 列表:
As expected, it generates free monoids, or Haskell lists:
toLst :: [a] -> Lst a
toLst as = \f -> foldMap f as
fromLst :: Lst a -> [a]
fromLst f = f (\a -> [a])toLst :: [a] -> Lst a
toLst as = \f -> foldMap f as
fromLst :: Lst a -> [a]
fromLst f = f (\a -> [a])
左 Kan 扩展是一个共数:
The left Kan extension is a coend:
LanKD a = ∫i A(K i, a) × D iLanKD a = ∫i A(K i, a) × D i
所以它转化为存在量词。象征性地:
so it translates to an existential quantifier. Symbolically:
Lan k d a = exists i. (k i -> a, d i)Lan k d a = exists i. (k i -> a, d i)
这可以使用 GADT 或使用通用量化数据构造函数在 Haskell 中进行编码:
This can be encoded in Haskell using GADTs, or using a universally quantified data constructor:
data Lan k d a = forall i. Lan (k i -> a) (d i)data Lan k d a = forall i. Lan (k i -> a) (d i)
这个数据结构的解释是它包含一个函数,该函数接受一些未指定的容器i并生成一个a. 它还有一个装有这些的容器i。由于您不知道is 是什么,因此您对此数据结构唯一能做的就是检索is 的容器,使用自然转换将其重新打包到仿函数定义的容器中k,然后调用该函数来获取a. 例如,如果d是一棵树,并且k是一个列表,则可以序列化该树,使用结果列表调用该函数,并获得一个a.
The interpretation of this data structure is that it contains a function that takes a container of some unspecified is and produces an a. It also has a container of those is. Since you have no idea what is are, the only thing you can do with this data structure is to retrieve the container of is, repack it into the container defined by the functor k using a natural transformation, and call the function to obtain the a. For instance, if d is a tree, and k is a list, you can serialize the tree, call the function with the resulting list, and obtain an a.
左 Kan 扩展可用于计算函子的右伴随。我们知道乘积函子的右伴随是指数,所以让我们尝试使用 Kan 扩展来实现它:
The left Kan extension can be used to calculate the right adjoint of a functor. We know that the right adjoint of the product functor is the exponential, so let’s try to implement it using the Kan extension:
type Exp a b = Lan ((,) a) I btype Exp a b = Lan ((,) a) I b
这确实与函数类型同构,正如以下一对函数所证明的那样:
This is indeed isomorphic to the function type, as witnessed by the following pair of functions:
toExp :: (a -> b) -> Exp a b
toExp f = Lan (f . fst) (I ())
fromExp :: Exp a b -> (a -> b)
fromExp (Lan f (I x)) = \a -> f (a, x)toExp :: (a -> b) -> Exp a b
toExp f = Lan (f . fst) (I ())
fromExp :: Exp a b -> (a -> b)
fromExp (Lan f (I x)) = \a -> f (a, x)
请注意,如前面在一般情况下所述,我们执行了以下步骤:(1) 检索 的容器x(这里,它只是一个简单的身份容器),并且函数f, (2) 使用之间的自然转换重新打包容器恒等函子和对函子,并且 (3) 称为函数f。
Notice that, as described earlier in the general case, we performed the following steps: (1) retrieved the container of x (here, it’s just a trivial identity container), and the function f, (2) repackaged the container using the natural transformation between the identity functor and the pair functor, and (3) called the function f.
Kan 扩展的一个有趣的应用是构建自由函子。它是以下实际问题的解决方案:假设您有一个类型构造函数 - 即对象的映射。是否可以基于此类型构造函数定义函子?换句话说,我们能否定义一个态射映射,将该类型构造函数扩展为成熟的endofunctor?
An interesting application of Kan extensions is the construction of a free functor. It’s the solution to the following practical problem: suppose you have a type constructor — that is a mapping of objects. Is it possible to define a functor based on this type constructor? In other words, can we define a mapping of morphisms that would extend this type constructor to a full-blown endofunctor?
关键的观察是类型构造函数可以被描述为一个函子,其域是离散范畴。离散范畴除了恒等态射之外没有其他态射。给定一个范畴C,我们总是可以构造一个离散范畴|C| 通过简单地丢弃所有非同一态射。F来自|C|的函子 到C就是对象的简单映射,或者我们在 Haskell 中称之为类型构造函数。还有一个J注入|C|的规范函子 换成C:它是对象上的恒等式(以及恒等态射)。Fdown的左 Kan 扩展J(如果存在)是C到C的函子:
The key observation is that a type constructor can be described as a functor whose domain is a discrete category. A discrete category has no morphisms other than the identity morphisms. Given a category C, we can always construct a discrete category |C| by simply discarding all non-identity morphisms. A functor F from |C| to C is then a simple mapping of objects, or what we call a type constructor in Haskell. There is also a canonical functor J that injects |C| into C: it’s an identity on objects (and on identity morphisms). The left Kan extension of F along J, if it exists, is then a functor for C to C:
LanJ F a = ∫i C(J i, a) × F iLanJ F a = ∫i C(J i, a) × F i
它被称为基于 的自由函子F。
It’s called a free functor based on F.
在 Haskell 中,我们将其写为:
In Haskell, we would write it as:
data FreeF f a = forall i. FMap (i -> a) (f i)data FreeF f a = forall i. FMap (i -> a) (f i)
事实上,对于任何类型的 constructor f,FreeF f都是一个函子:
Indeed, for any type constructor f, FreeF f is a functor:
instance Functor (FreeF f) where
fmap g (FMap h fi) = FMap (g . h) fiinstance Functor (FreeF f) where
fmap g (FMap h fi) = FMap (g . h) fi
正如您所看到的,自由函子通过记录函数及其参数来伪造函数的提升。它通过记录其组成来累积提升的函数。自动满足函子规则。这种结构被用在论文《Freer Monads, More Extensible Effects》中。
As you can see, the free functor fakes the lifting of a function by recording both the function and its argument. It accumulates the lifted functions by recording their composition. Functor rules are automatically satisfied. This construction was used in a paper Freer Monads, More Extensible Effects.
或者,我们可以使用正确的 Kan 扩展来达到相同的目的:
Alternatively, we can use the right Kan extension for the same purpose:
newtype FreeF f a = FreeF (forall i. (a -> i) -> f i)newtype FreeF f a = FreeF (forall i. (a -> i) -> f i)
很容易检查这确实是一个函子:
It’s easy to check that this is indeed a functor:
instance Functor (FreeF f) where
fmap g (FreeF r) = FreeF (\bi -> r (bi . g))instance Functor (FreeF f) where
fmap g (FreeF r) = FreeF (\bi -> r (bi . g))
如果一个范畴的对象形成一个集合,那么该范畴就很小。但我们知道有些东西比集合更大。众所周知,所有集合的集合不能在标准集合论(Zermelo-Fraenkel 理论,可选地用选择公理进行增强)内形成。所以所有集合的范畴一定很大。有一些数学技巧,比如格洛腾迪克宇宙,可以用来定义超越集合的集合。这些技巧让我们谈论大范畴。
A category is small if its objects form a set. But we know that there are things larger than sets. Famously, a set of all sets cannot be formed within the standard set theory (the Zermelo-Fraenkel theory, optionally augmented with the Axiom of Choice). So a category of all sets must be large. There are mathematical tricks like Grothendieck universes that can be used to define collections that go beyond sets. These tricks let us talk about large categories.
如果任意两个对象之间的态射形成一个集合,则范畴局部较小。如果它们不形成一个集合,我们就必须重新考虑一些定义。特别是,如果我们甚至无法从集合中挑选态射,那么组合态射意味着什么?解决方案是通过用其他范畴 V 中的对象替换 hom-sets(Set 中的对象)来引导我们自己。不同之处在于,一般来说,对象没有元素,因此我们不再被允许谈论个体态射。我们必须根据可以对整个 hom 对象执行的操作来定义丰富范畴的所有属性。为了做到这一点,提供 hom-objects 的范畴必须有额外的结构——它必须是一个幺半群范畴。如果我们将这个幺半群范畴称为V,我们就可以谈论在V上丰富的范畴C。
A category is locally small if morphisms between any two objects form a set. If they don’t form a set, we have to rethink a few definitions. In particular, what does it mean to compose morphisms if we can’t even pick them from a set? The solution is to bootstrap ourselves by replacing hom-sets, which are objects in Set, with objects from some other category V. The difference is that, in general, objects don’t have elements, so we are no longer allowed to talk about individual morphisms. We have to define all properties of an enriched category in terms of operations that can be performed on hom-objects as a whole. In order to do that, the category that provides hom-objects must have additional structure — it must be a monoidal category. If we call this monoidal category V, we can talk about a category C enriched over V.
除了大小原因之外,我们可能有兴趣将 hom-set 推广到比单纯的集合具有更多结构的东西。例如,传统范畴没有对象之间距离的概念。两个对象要么通过态射连接,要么不连接。连接到给定对象的所有对象都是其邻居。与现实生活不同;在某个范畴中,朋友的朋友的朋友与我的关系就像我的知己一样。在适当丰富的范畴中,我们可以定义对象之间的距离。
Beside size reasons, we might be interested in generalizing hom-sets to something that has more structure than mere sets. For instance, a traditional category doesn’t have the notion of a distance between objects. Two objects are either connected by morphisms or not. All objects that are connected to a given object are its neighbors. Unlike in real life; in a category, a friend of a friend of a friend is as close to me as my bosom buddy. In a suitably enriched category, we can define distances between objects.
还有一个非常实际的原因需要获得丰富范畴的经验,那是因为nLab是一个非常有用的在线分类知识源,它主要是根据丰富范畴编写的。
There is one more very practical reason to get some experience with enriched categories, and that’s because a very useful online source of categorical knowledge, the nLab, is written mostly in terms of enriched categories.
在构建丰富的范畴时,我们必须记住,当我们用Set替换幺半群范畴,用hom-sets 替换 hom-objects 时,我们应该能够恢复通常的定义。实现这一目标的最佳方法是从通常的定义开始,并以无点的方式不断重新表述它们——也就是说,不命名集合的元素。
When constructing an enriched category we have to keep in mind that we should be able to recover the usual definitions when we replace the monoidal category with Set and hom-objects with hom-sets. The best way to accomplish this is to start with the usual definitions and keep reformulating them in a point-free manner — that is, without naming elements of sets.
让我们从组合的定义开始。通常,它需要一对态射,一个来自C(b, c),一个来自C(a, b),并将其映射到来自 的态射C(a, c)。换句话说,它是一个映射:
Let’s start with the definition of composition. Normally, it takes a pair of morphisms, one from C(b, c) and one from C(a, b) and maps it to a morphism from C(a, c). In other words it’s a mapping:
C(b, c) × C(a, b) -> C(a, c)C(b, c) × C(a, b) -> C(a, c)
这是集合之间的函数——其中一个是两个 hom 集的笛卡尔积。通过用更通用的东西替换笛卡尔积,可以轻松推广该公式。分类积可以工作,但我们可以更进一步,使用完全通用的张量积。
This is a function between sets — one of them being the cartesian product of two hom-sets. This formula can be easily generalized by replacing cartesian product with something more general. A categorical product would work, but we can go even further and use a completely general tensor product.
接下来是恒等态射。我们可以使用单例集1中的函数来定义它们,而不是从 hom-sets 中选取单个元素:
Next come the identity morphisms. Instead of picking individual elements from hom-sets, we can define them using functions from the singleton set 1:
ja :: 1 -> C(a, a)ja :: 1 -> C(a, a)
同样,我们可以用终端对象替换单例集,但我们可以更进一步,用i张量积的单位替换它。
Again, we could replace the singleton set with the terminal object, but we can go even further by replacing it with the unit i of the tensor product.
正如您所看到的,从某些幺半群V类中获取的对象是 hom-set 替换的良好候选者。
As you can see, objects taken from some monoidal category V are good candidates for hom-set replacement.
我们之前讨论过幺半群范畴,但值得重申一下定义。幺半群范畴定义了一个双函子的张量积:
We’ve talked about monoidal categories before, but it’s worth restating the definition. A monoidal category defines a tensor product that is a bifunctor:
⊗ :: V × V -> V⊗ :: V × V -> V
我们希望张量积具有结合性,但这足以满足结合性直至自然同构。这种同构称为关联子。其组成部分是:
We want the tensor product to be associative, but it’s enough to satisfy associativity up to natural isomorphism. This isomorphism is called the associator. Its components are:
αa b c :: (a ⊗ b) ⊗ c -> a ⊗ (b ⊗ c)αa b c :: (a ⊗ b) ⊗ c -> a ⊗ (b ⊗ c)
这三个论证都必须是自然的。
It must be natural in all three arguments.
幺半群范畴还必须定义一个特殊的单位对象i,作为张量积的单位;再次,直到自然同构。这两个同构分别称为左、右单位,它们的分量为:
A monoidal category must also define a special unit object i that serves as the unit of the tensor product; again, up to natural isomorphism. The two isomorphisms are called, respectively, the left and the right unitor, and their components are:
λa :: i ⊗ a -> a
ρa :: a ⊗ i -> aλa :: i ⊗ a -> a
ρa :: a ⊗ i -> a
关联者和单位者必须满足一致性条件:
The associator and the unitors must satisfy coherence conditions:
如果一个幺半群范畴的分量存在自然同构,则称为对称范畴:
A monoidal category is called symmetric if there is a natural isomorphism with components:
γa b :: a ⊗ b -> b ⊗ aγa b :: a ⊗ b -> b ⊗ a
其“平方为一”:
whose “square is one”:
γb a ∘ γa b = ida⊗bγb a ∘ γa b = ida⊗b
且与幺半群结构一致。
and which is consistent with the monoidal structure.
关于幺半群范畴的一个有趣的事情是,您可以将内部 hom(函数对象)定义为张量积的右伴随。您可能还记得函数对象或指数的标准定义是通过分类乘积的右伴随来实现的。对于任何一对对象都存在这样一个对象的范畴称为笛卡尔闭范畴。这是定义幺半群范畴中的内部 hom 的附加项:
An interesting thing about monoidal categories is that you may be able to define the internal hom (the function object) as the right adjoint to the tensor product. You may recall that the standard definition of the function object, or the exponential, was through the right adjoint to the categorical product. A category in which such an object existed for any pair of objects was called cartesian closed. Here is the adjunction that defines the internal hom in a monoidal category:
V(a ⊗ b, c) ~ V(a, [b, c])V(a ⊗ b, c) ~ V(a, [b, c])
遵循GM Kelly 的建议,我使用[b, c]内部 hom 的符号。这个加法的单位是自然变换,其组成部分称为求值态射:
Following G. M. Kelly, I’m using the notation [b, c] for the internal hom. The counit of this adjunction is the natural transformation whose components are called evaluation morphisms:
εa b :: ([a, b] ⊗ a) -> bεa b :: ([a, b] ⊗ a) -> b
请注意,如果张量积不对称,我们可以[[a, c]]使用以下附加词定义另一个内部 hom,用 表示:
Notice that, if the tensor product is not symmetric, we may define another internal hom, denoted by [[a, c]], using the following adjunction:
V(a ⊗ b, c) ~ V(b, [[a, c]])V(a ⊗ b, c) ~ V(b, [[a, c]])
两者均被定义的幺半群范畴称为双闭范畴。非双闭范畴的一个例子是Set中的内函子范畴,其中函子组合充当张量积。这就是我们用来定义 monad 的范畴。
A monoidal category in which both are defined is called biclosed. An example of a category that is not biclosed is the category of endofunctors in Set, with functor composition serving as tensor product. That’s the category we used to define monads.
在幺半群范畴V上丰富的范畴C用 hom 对象替换 hom 集。对于C中的每对对象,我们将V中的一个对象关联起来。我们对 hom 对象使用与 hom 集相同的表示法,但要理解它们不包含态射。另一方面,V是一个具有 hom 集和态射的常规(非丰富)范畴。所以我们并没有完全摆脱场景——我们只是把它们隐藏起来。abC(a, b)
A category C enriched over a monoidal category V replaces hom-sets with hom-objects. To every pair of objects a and b in C we associate an object C(a, b) in V. We use the same notation for hom-objects as we used for hom-sets, with the understanding that they don’t contain morphisms. On the other hand, V is a regular (non-enriched) category with hom-sets and morphisms. So we are not entirely rid of sets — we just swept them under the rug.
由于我们不能谈论C中的单个态射,态射的复合被V中的态射族所取代:
Since we cannot talk about individual morphisms in C, composition of morphisms is replaced by a family of morphisms in V:
∘ :: C(b, c) ⊗ C(a, b) -> C(a, c)∘ :: C(b, c) ⊗ C(a, b) -> C(a, c)
类似地,恒等态射被V中的态射族所取代:
Similarly, identity morphisms are replaced by a family of morphisms in V:
ja :: i -> C(a, a)ja :: i -> C(a, a)
其中是Vi中的张量单位。
where i is the tensor unit in V.
组合的结合性根据V中的结合子定义:
Associativity of composition is defined in terms of the associator in V:
单位定律同样以单位单位表示:
Unit laws are likewise expressed in terms of unitors:
预序被定义为一个薄范畴,其中每个 hom-set 要么是空的,要么是单例的。我们将非空集解释为小于或等于 的C(a, b)证明。这样的范畴可以解释为比仅包含两个对象 0 和 1(有时称为 False 和 True)的非常简单的幺半群范畴丰富。除了强制的恒等态射之外,这个范畴还有一个从 0 到 1 的单一态射,我们称之为。可以在其中建立一个简单的幺半群结构,用张量乘积对 0 和 1 的简单算术进行建模(即唯一的非零乘积是)。该范畴的恒等对象为1。这是一个严格的幺半群范畴,即关联子和单位子都是恒等态射。ab0->11⊗1
A preorder is defined as a thin category, one in which every hom-set is either empty or a singleton. We interpret a non-empty set C(a, b) as the proof that a is less than or equal to b. Such a category can be interpreted as enriched over a very simple monoidal category that contains just two objects, 0 and 1 (sometimes called False and True). Besides the mandatory identity morphisms, this category has a single morphism going from 0 to 1, let’s call it 0->1. A simple monoidal structure can be established in it, with the tensor product modeling the simple arithmetic of 0 and 1 (i.e., the only non-zero product is 1⊗1). The identity object in this category is 1. This is a strict monoidal category, that is, the associator and the unitors are identity morphisms.
由于在预购中,hom 集要么是空的,要么是单例,因此我们可以轻松地将其替换为我们小范畴中的 hom 对象。丰富的前序CC(a, b)对于任意一对对象a和都有一个 hom 对象b。如果a小于或等于b,则该对象为 1;否则为 0。
Since in a preorder the-hom set is either empty or a singleton, we can easily replace it with a hom-object from our tiny category. The enriched preorder C has a hom-object C(a, b) for any pair of objects a and b. If a is less than or equal to b, this object is 1; otherwise it’s 0.
我们来看看组成。任何两个对象的张量积都是 0,除非它们都是 1,在这种情况下它是 1。如果它是 0,那么我们对于组合态射有两个选择:它可以是id0或0->1。但如果是 1,那么唯一的选择就是id1。将其转换回关系,这表示 ifa <= b和b <= cthen a <= c,这正是我们需要的传递性定律。
Let’s have a look at composition. The tensor product of any two objects is 0, unless both of them are 1, in which case it’s 1. If it’s 0, then we have two options for the composition morphism: it could be either id0 or 0->1. But if it’s 1, then the only option is id1. Translating this back to relations, this says that if a <= b and b <= c then a <= c, which is exactly the transitivity law we need.
那身份呢?这是从 1 到 的态射C(a, a)。从 1 开始只有一个态射,这就是恒等式id1,所以C(a, a)一定是 1。这意味着a <= a,这是先序的自反律。因此,如果我们将预序实现为丰富的范畴,那么传递性和自反性都会自动强制执行。
What about the identity? It’s a morphism from 1 to C(a, a). There is only one morphism going from 1, and that’s the identity id1, so C(a, a) must be 1. It means that a <= a, which is the reflexivity law for a preorder. So both transitivity and reflexivity are automatically enforced, if we implement a preorder as an enriched category.
威廉·劳维尔( William Lawvere)提出了一个有趣的例子。他注意到度量空间可以使用丰富的范畴来定义。度量空间定义任意两个对象之间的距离。该距离是一个非负实数。将无穷大作为可能值包含起来很方便。如果距离无限大,则无法从起始对象到达目标对象。
An interesting example is due to William Lawvere. He noticed that metric spaces can be defined using enriched categories. A metric space defines a distance between any two objects. This distance is a non-negative real number. It’s convenient to include inifinity as a possible value. If the distance is infinite, there is no way of getting from the starting object to the target object.
有一些明显的属性必须通过距离来满足。其中之一是物体到自身的距离必须为零。另一种是三角不等式:直接距离不大于中间停靠点的距离之和。我们不要求距离是对称的,这乍一看可能很奇怪,但正如劳维尔解释的那样,你可以想象在一个方向上你正在上坡,而在另一个方向上你正在下坡。在任何情况下,对称性都可以作为附加约束稍后施加。
There are some obvious properties that have to be satisfied by distances. One of them is that the distance from an object to itself must be zero. The other is the triangle inequality: the direct distance is no larger than the sum of distances with intermediate stops. We don’t require the distance to be symmetric, which might seem weird at first but, as Lawvere explained, you can imagine that in one direction you’re walking uphill, while in the other you’re going downhill. In any case, symmetry may be imposed later as an additional constraint.
那么如何将度量空间转换为分类语言呢?我们必须构建一个范畴,其中 hom-objects 是距离。请注意,距离不是态射,而是 hom 对象。hom 对象怎么可能是数字呢?只有当我们能够构造一个幺半群范畴V,其中这些数字是对象时。非负实数(加上无穷大)形成全序,因此可以将它们视为薄范畴。x两个这样的数和之间的态射y当且仅当存在x >= y(注意:这与传统上在预序定义中使用的方向相反)。幺半群结构是通过加法给出的,以零作为单位对象。换句话说,两个数字的张量积是它们的和。
So how can a metric space be cast into a categorical language? We have to construct a category in which hom-objects are distances. Mind you, distances are not morphisms but hom-objects. How can a hom-object be a number? Only if we can construct a monoidal category V in which these numbers are objects. Non-negative real numbers (plus infinity) form a total order, so they can be treated as a thin category. A morphism between two such numbers x and y exists if and only if x >= y (note: this is the opposite direction to the one traditionally used in the definition of a preorder). The monoidal structure is given by addition, with zero serving as the unit object. In other words, the tensor product of two numbers is their sum.
度量空间是在这种幺半群范畴上丰富的范畴。C(a, b)从对象a到 的hom 对象b是一个非负数(可能是无穷大),我们将其称为从a到 的距离b。让我们看看在这个范畴中我们得到了什么身份和组成。
A metric space is a category enriched over such monoidal category. A hom-object C(a, b) from object a to b is a non-negative (possibly infinite) number that we will call the distance from a to b. Let’s see what we get for identity and composition in such a category.
根据我们的定义,从张量单位(即数字零)到 hom 对象的态C(a, a)射是以下关系:
By our definitions, a morphism from the tensorial unit, which is the number zero, to a hom-object C(a, a) is the relation:
0 >= C(a, a)0 >= C(a, a)
由于是一个非负数,这个条件告诉我们到 到C(a, a)的距离始终为零。查看!aa
Since C(a, a) is a non-negative number, this condition tells us that the distance from a to a is always zero. Check!
现在我们来谈谈构图。我们从两个相邻的 hom 对象的张量积开始,C(b, c)⊗C(a, b)。我们将张量积定义为两个距离之和。组合是V中从该乘积 到 的态射C(a, c)。V中的态射被定义为大于或等于关系。换句话说,从a到b和从b到 的距离c之和大于或等于从a到 的距离c。但这只是标准的三角不等式。查看!
Now let’s talk about composition. We start with the tensor product of two abutting hom-objects, C(b, c)⊗C(a, b). We have defined the tensor product as the sum of the two distances. Composition is a morphism in V from this product to C(a, c). A morphism in V is defined as the greater-or-equal relation. In other words, the sum of distances from a to b and from b to c is greater than or equal to the distance from a to c. But that’s just the standard triangle inequality. Check!
通过根据丰富的范畴重新构建度量空间,我们“免费”得到了三角不等式和零自距离。
By re-casting the metric space in terms of an enriched category, we get the triangle inequality and the zero self-distance “for free.”
函子的定义涉及态射的映射。在丰富的环境中,我们没有个体态射的概念,因此我们必须批量处理 hom 对象。Hom 对象是幺半群范畴V中的对象,并且我们可以使用它们之间的态射。因此,当范畴之间的丰富函子在同一幺半群范畴V上丰富时,定义范畴之间的丰富函子是有意义的。然后我们可以使用V中的态射来映射两个丰富范畴之间的 hom 对象。
The definition of a functor involves the mapping of morphisms. In the enriched setting, we don’t have the notion of individual morphisms, so we have to deal with hom-objects in bulk. Hom-objects are objects in a monoidal category V, and we have morphisms between them at our disposal. It therefore makes sense to define enriched functors between categories when they are enriched over the same monoidal category V. We can then use morphisms in V to map the hom-objects between two enriched categories.
两个范畴C和D之间的丰富函子 ,除了将对象映射到对象之外,还为C中的每对对象分配V中的态射:F
An enriched functor F between two categories C and D, besides mapping objects to objects, also assigns, to every pair of objects in C, a morphism in V:
Fa b :: C(a, b) -> D(F a, F b)Fa b :: C(a, b) -> D(F a, F b)
函子是一种保留结构的映射。对于常规函子来说,这意味着保留组成和同一性。在丰富的设置中,构图的保留意味着下图可以交换:
A functor is a structure-preserving mapping. For regular functors it meant preserving composition and identity. In the enriched setting, the preservation of composition means that the following diagram commute:
恒等性的保留被V中“选择”恒等性的态射的保留所取代:
The preservation of identity is replaced by the preservation of the morphisms in V that “select” the identity:
闭合对称幺半群范畴可以通过用内部 hom 替换 hom 集来自我丰富(参见上面的定义)。为了实现这一点,我们必须定义内部角的合成法则。换句话说,我们必须实现具有以下签名的态射:
A closed symmetric monoidal category may be self-enriched by replacing hom-sets with internal homs (see the definition above). To make this work, we have to define the composition law for internal homs. In other words, we have to implement a morphism with the following signature:
[b, c] ⊗ [a, b] -> [a, c][b, c] ⊗ [a, b] -> [a, c]
这与任何其他编程任务没有太大区别,只是在范畴论中,我们通常使用无点实现。我们首先指定它应该是其元素的集合。在本例中,它是 hom-set 的成员:
This is not much different from any other programming task, except that, in category theory, we usually use point free implementations. We start by specifying the set whose element it’s supposed to be. In this case, it’s a member of the hom-set:
V([b, c] ⊗ [a, b], [a, c])V([b, c] ⊗ [a, b], [a, c])
这个 hom-set 同构于:
This hom-set is isomorphic to:
V(([b, c] ⊗ [a, b]) ⊗ a, c)V(([b, c] ⊗ [a, b]) ⊗ a, c)
我刚刚使用了定义内部 hom 的附加词[a, c]。如果我们可以在这个新集合中构建一个态射,则附加将把我们指向原始集合中的态射,然后我们可以将其用作复合。我们通过组合几个我们可以使用的态射来构造这个态射。首先,我们可以使用关联器α[b, c] [a, b] a重新关联左侧的表达式:
I just used the adjunction that defined the internal hom [a, c]. If we can build a morphism in this new set, the adjunction will point us at the morphism in the original set, which we can then use as composition. We construct this morphism by composing several morphisms that are at our disposal. To begin with, we can use the associator α[b, c] [a, b] a to reassociate the expression on the left:
([b, c] ⊗ [a, b]) ⊗ a -> [b, c] ⊗ ([a, b] ⊗ a)([b, c] ⊗ [a, b]) ⊗ a -> [b, c] ⊗ ([a, b] ⊗ a)
我们可以在它后面加上附加词的共同单位εa b:
We can follow it with the co-unit of the adjunction εa b:
[b, c] ⊗ ([a, b] ⊗ a) -> [b, c] ⊗ b[b, c] ⊗ ([a, b] ⊗ a) -> [b, c] ⊗ b
并再次使用 计数εb c来到达c。我们因此构造了一个态射:
And use the counit εb c again to get to c. We have thus constructed a morphism:
εb c . (id[b, c] ⊗ εa b) . α[b, c] [a, b] aεb c . (id[b, c] ⊗ εa b) . α[b, c] [a, b] a
这是 hom-set 的一个元素:
that is an element of the hom-set:
V(([b, c] ⊗ [a, b]) ⊗ a, c)V(([b, c] ⊗ [a, b]) ⊗ a, c)
该附加将为我们提供我们正在寻找的合成法则。
The adjunction will give us the composition law we were looking for.
同样,身份:
Similarly, the identity:
ja :: i -> [a, a]ja :: i -> [a, a]
是以下 hom-set 的成员:
is a member of the following hom-set:
V(i, [a, a])V(i, [a, a])
通过附加,它同构于:
which is isomorphic, through adjunction, to:
V(i ⊗ a, a) V(i ⊗ a, a)
我们知道这个 hom-set 包含左恒等式λa。我们可以将ja其定义为附加词下的图像。
We know that this hom-set contains the left identity λa. We can define ja as its image under the adjunction.
自我丰富的一个实际例子是范畴Set,它充当编程语言中类型的原型。我们之前已经看到,它是关于笛卡尔积的封闭幺半群范畴。在Set中,任何两个集合之间的 hom-set 本身就是一个集合,因此它是Set中的一个对象。我们知道它与指数集同构,因此外宏和内宏是等价的。现在我们还知道,通过自我丰富,我们可以使用指数集作为 hom 对象,并用指数对象的笛卡尔积来表达组合。
A practical example of self-enrichment is the category Set that serves as the prototype for types in programming languages. We’ve seen before that it’s a closed monoidal category with respect to cartesian product. In Set, the hom-set between any two sets is itself a set, so it’s an object in Set. We know that it’s isomorphic to the exponential set, so the external and the internal homs are equivalent. Now we also know that, through self-enrichment, we can use the exponential set as the hom-object and express composition in terms of cartesian products of exponential objects.
我在Cat的上下文中讨论了 2-categories ,即(小)范畴的范畴。范畴之间的态射是函子,但还有一个额外的结构:函子之间的自然变换。在 2 范畴中,对象通常称为零单元;态射,1-细胞;以及态射之间的态射,2-细胞。在Cat中,0 单元是范畴,1 单元是函子,2 单元是自然变换。
I talked about 2-categories in the context of Cat, the category of (small) categories. The morphisms between categories are functors, but there is an additional structure: natural transformations between functors. In a 2-category, the objects are often called zero-cells; morphisms, 1-cells; and morphisms between morphisms, 2-cells. In Cat the 0-cells are categories, 1-cells are functors, and 2-cells are natural transformations.
但请注意,两个范畴之间的函子也形成一个范畴;所以,在Cat中,我们确实有一个hom-category而不是 hom-set。事实证明,就像Set可以被视为在Set 上丰富的范畴一样,Cat也可以被视为在Cat上丰富的范畴。更一般地说,就像每个范畴都可以被视为在Set上丰富一样,每个 2 范畴都可以被视为在Cat上丰富。
But notice that functors between two categories form a category too; so, in Cat, we really have a hom-category rather than a hom-set. It turns out that, just like Set can be treated as a category enriched over Set, Cat can be treated as a category enriched over Cat. Even more generally, just like every category can be treated as enriched over Set, every 2-category can be considered enriched over Cat.
我意识到我们可能会远离编程而深入研究核心数学。但你永远不知道编程领域的下一次重大革命可能会带来什么,以及理解它可能需要什么样的数学。有一些非常有趣的想法,比如具有连续时间的函数反应式编程、具有依赖类型的 Haskell 类型系统的扩展,或者对编程中同伦类型理论的探索。
I realize that we might be getting away from programming and diving into hard-core math. But you never know what the next big revolution in programming might bring and what kind of math might be necessary to understand it. There are some very interesting ideas going around, like functional reactive programming with its continuous time, the extention of Haskell’s type system with dependent types, or the exploration on homotopy type theory in programming.
到目前为止,我一直在随意地用值集来识别类型。这并不完全正确,因为这种方法没有考虑到这样一个事实:在编程中,我们计算值,而计算是一个需要时间的过程,在极端情况下,可能不会终止。发散计算是每种图灵完备语言的一部分。
So far I’ve been casually identifying types with sets of values. This is not strictly correct, because such approach doesn’t take into account the fact that, in programming, we compute values, and the computation is a process that takes time and, in extreme cases, might not terminate. Divergent computations are part of every Turing-complete language.
还有一些根本原因可以解释为什么集合论可能不适合作为计算机科学甚至数学本身的基础。一个很好的类比是,集合论是与特定体系结构相关的汇编语言。如果您想在不同的架构上运行数学,则必须使用更通用的工具。
There are also foundational reasons why set theory might not be the best fit as the basis for computer science or even math itself. A good analogy is that of set theory being the assembly language that is tied to a particular architecture. If you want to run your math on different architectures, you have to use more general tools.
一种可能性是使用空格代替集合。空间具有更多的结构,并且可以在不依赖集合的情况下进行定义。通常与空间相关的一件事是拓扑,它对于定义连续性等事物是必要的。你猜对了,传统的拓扑方法是通过集合论。特别是,子集的概念是拓扑的核心。毫不奇怪,范畴论学家将这个想法推广到Set以外的范畴。具有恰好可以替代集合论的属性的范畴类型称为拓扑(复数:topoi),除其他外,它提供了子集的广义概念。
One possibility is to use spaces in place of sets. Spaces come with more structure, and may be defined without recourse to sets. One thing usually associated with spaces is topology, which is necessary to define things like continuity. And the conventional approach to topology is, you guessed it, through set theory. In particular, the notion of a subset is central to topology. Not surprisingly, category theorists generalized this idea to categories other than Set. The type of category that has just the right properties to serve as a replacement for set theory is called a topos (plural: topoi), and it provides, among other things, a generalized notion of a subset.
让我们首先尝试使用函数而不是元素来表达子集的想法。f某个集合中的任何函数都a定义了下图像的b子集。但有许多函数定义相同的子集。我们需要更具体。首先,我们可能会关注单射函数——不会将多个元素压缩为一个的函数。内射函数将一组“注入”到另一组中。对于有限集,您可以将单射函数可视化为将一个集合的元素连接到另一个集合的元素的平行箭头。当然,第一组不能大于第二组,否则箭头必然会聚。仍然存在一些歧义:可能存在另一个集合和来自该集合的另一个单射函数来选择相同的子集。但您可以轻松地说服自己,这样的集合必须同构于。我们可以使用这个事实将子集定义为通过其域的同构相关的单射函数族。更准确地说,我们说两个单射函数:bafa'f'ba
Let’s start by trying to express the idea of a subset using functions rather than elements. Any function f from some set a to b defines a subset of b–that of the image of a under f. But there are many functions that define the same subset. We need to be more specific. To begin with, we might focus on functions that are injective — ones that don’t smush multiple elements into one. Injective functions “inject” one set into another. For finite sets, you may visualize injective functions as parallel arrows connecting elements of one set to elements of another. Of course, the first set cannot be larger than the second set, or the arrows would necessarily converge. There is still some ambiguity left: there may be another set a' and another injective function f' from that set to b that picks the same subset. But you can easily convince yourself that such a set would have to be isomorphic to a. We can use this fact to define a subset as a family of injective functions that are related by isomorphisms of their domains. More precisely, we say that two injective functions:
f :: a -> b
f':: a'-> bf :: a -> b
f':: a'-> b
如果存在同构,则它们是等价的:
are equivalent if there is an isomorphism:
h :: a -> a'h :: a -> a'
这样:
such that:
f = f' . hf = f' . h
这样的一系列等效注入定义了 的子集b。
Such a family of equivalent injections defines a subset of b.
如果我们用单态代替单射函数,这个定义就可以提升到任意范畴。只是提醒您,m从a到 的单态b是由其通用属性定义的。对于任何对象c和任何态射对:
This definition can be lifted to an arbitrary category if we replace injective functions with monomorphism. Just to remind you, a monomorphism m from a to b is defined by its universal property. For any object c and any pair of morphisms:
g :: c -> a
g':: c -> ag :: c -> a
g':: c -> a
这样:
such that:
m . g = m . g'm . g = m . g'
一定是这样的g = g'。
it must be that g = g'.
m 在集合上,如果我们考虑一个函数不是单态的意味着什么,这个定义就更容易理解。它将 的两个不同元素映射a到 的单个元素b。然后我们可以找到两个函数g,并且g'它们仅在这两个元素上有所不同。后合成m将掩盖这种差异。
On sets, this definition is easier to understand if we consider what it would mean for a function m not to be a monomorphism. It would map two different elements of a to a single element of b. We could then find two functions g and g' that differ only at those two elements. The postcomposition with m would then mask this difference.
还有另一种定义子集的方法:使用称为特征函数的单个函数。χ它是从集合b到二元素集合的函数Ω。该集合中的一个元素被指定为“真”,另一个被指定为“假”。该函数将“true”分配给那些属于子集成员的元素b,并将“false”分配给那些不是子集成员的元素。
There is another way of defining a subset: using a single function called the characteristic function. It’s a function χ from the set b to a two-element set Ω. One element of this set is designated as “true” and the other as “false.” This function assigns “true” to those elements of b that are members of the subset, and “false” to those that aren’t.
仍然需要具体说明将 的元素指定Ω为“true”的含义。我们可以使用标准技巧:使用单例中设置为 的函数Ω。我们将调用这个函数true:
It remains to specify what it means to designate an element of Ω as “true.” We can use the standard trick: use a function from a singleton set to Ω. We’ll call this function true:
true :: 1 -> Ωtrue :: 1 -> Ω
这些定义可以以这样的方式组合:它们不仅定义子对象是什么,而且还定义特殊对象Ω而不谈论元素。我们的想法是,我们希望态射true代表一个“通用”子对象。在Set中,它从二元素 set 中选取一个单元素子集Ω。这很通用。它显然是一个真子集,因为Ω还有一个不属于该子集的元素。
These definitions can be combined in such a way that they not only define what a subobject is, but also define the special object Ω without talking about elements. The idea is that we want the morphism true to represent a “generic” subobject. In Set, it picks a single-element subset from a two-element set Ω. This is as generic as it gets. It’s clearly a proper subset, because Ω has one more element that’s not in that subset.
在更一般的设置中,我们定义true为从终端对象到分类对象的 Ω单态。但我们必须定义分类对象。我们需要一个通用属性来将该对象与特征函数联系起来。事实证明,在Settrue中,沿着特征函数的回拉χ定义了子集a和将其嵌入到 中的单射函数b。回调图如下:
In a more general setting, we define true to be a monomorphism from the terminal object to the classifying object Ω. But we have to define the classifying object. We need a universal property that links this object to the characteristic function. It turns out that, in Set, the pullback of true along the characteristic function χ defines both the subset a and the injective function that embeds it in b. Here’s the pullback diagram:
我们来分析一下这张图。回调方程为:
Let’s analyze this diagram. The pullback equation is:
true . unit = χ . ftrue . unit = χ . f
该函数true . unit将 的每个元素映射a为“true”。因此f,必须将 的所有元素映射a到 的那些元素b为χ“true”。根据定义,这些是由特征函数 指定的子集的元素χ。所以 的图像f确实是所讨论的子集。回调的普遍性保证了它f是单射的。
The function true . unit maps every element of a to “true.” Therefore f must map all elements of a to those elements of b for which χ is “true.” These are, by definition, the elements of the subset that is specified by the characteristic function χ. So the image of f is indeed the subset in question. The universality of the pullback guarantees that f is injective.
该回调图可用于定义除Set之外的范畴中的分类对象。这样的范畴必须有一个终端对象,这将让我们定义单态性true。它还必须具有回调——实际要求是它必须具有所有有限限制(回调是有限限制的一个示例)。Ω在这些假设下,我们通过以下属性定义分类对象:对于每个单态,f都有一个唯一的态射χ来完成回调图。
This pullback diagram can be used to define the classifying object in categories other than Set. Such a category must have a terminal object, which will let us define the monomorphism true. It must also have pullbacks — the actual requirement is that it must have all finite limits (a pullback is an example of a finite limit). Under those assumptions, we define the classifying object Ω by the property that, for every monomorphism f there is a unique morphism χ that completes the pullback diagram.
我们来分析一下最后一条语句。当我们构造回调时,我们会得到三个对象Ω、b和1;和两个态射,true并且χ。回拉的存在意味着我们可以找到最好的此类对象a,配备两个态射f和unit(后者由终端对象的定义唯一确定),使图可交换。
Let’s analyze the last statement. When we construct a pullback, we are given three objects Ω, b and 1; and two morphisms, true and χ. The existence of a pullback means that we can find the best such object a, equipped with two morphisms f and unit (the latter is uniquely determined by the definition of the terminal object), that make the diagram commute.
这里我们正在求解不同的方程组。我们正在求解Ω和 ,true同时改变a 和 b。对于给定的a和b可能存在也可能不存在单态f::a->b。但如果有的话,我们希望它是一些回调χ。此外,我们希望它χ由 唯一确定f。
Here we are solving a different system of equations. We are solving for Ω and true while varying both a and b. For a given a and b there may or may not be a monomorphism f::a->b. But if there is one, we want it to be a pullback of some χ. Moreover, we want this χ to be uniquely determined by f.
f我们不能说单态和特征函数之间存在一一对应的关系χ,因为回调只有在同构时才是唯一的。但请记住我们之前将子集定义为等效注入系列。我们可以通过将 的子对象定义b为 的等价单态族来概括它b。这个单态家族与我们图中的等价回调家族一一对应。
We can’t say that there is a one-to-one correspondence between monomorphisms f and characteristic functions χ, because a pullback is only unique up to isomorphism. But remember our earlier definition of a subset as a family of equivalent injections. We can generalize it by defining a subobject of b as a family of equivalent monomorphisms to b. This family of monomorphisms is in one-to-one corrpespondence with the family of equivalent pullbacks of our diagram.
b因此,我们可以将, ,的一组子对象定义Sub(b)为单态族,并且看到它与从b到 的态射集同构Ω:
We can thus define a set of subobjects of b, Sub(b), as a family of monomorphisms, and see that it is isomorphic to the set of morphisms from b to Ω:
Sub(b) ≅ C(b, Ω)Sub(b) ≅ C(b, Ω)
这恰好是两个函子的自然同构。换句话说,Sub(-)是一个可表示(逆变)函子,其表示是对象 Ω。
This happens to be a natural isomorphism of two functors. In other words, Sub(-) is a representable (contravariant) functor whose representation is the object Ω.
拓扑是一个范畴:
A topos is a category that:
Ω。Ω.这组属性使拓扑成为大多数应用中Set的必备条件。它还具有根据其定义得出的附加属性。例如,拓扑具有所有有限余界,包括初始对象。
This set of properties makes a topos a shoe-in for Set in most applications. It also has additional properties that follow from its definition. For instance, a topos has all finite colimits, including the initial object.
将子对象分类器定义为终端对象的两个副本的余积(和)是很诱人的——这就是Set中的内容——但我们希望比这更通用。满足这一点的 Topoi 称为布尔型。
It would be tempting to define the subobject classifier as a coproduct (sum) of two copies of the terminal object –that’s what it is in Set— but we want to be more general than that. Topoi in which this is true are called Boolean.
在集合论中,特征函数可以被解释为定义集合元素的属性——对于某些元素为真而对于其他元素为假的谓词。该谓词isEven从自然数集中选择偶数的子集。在拓扑中,我们可以将谓词的概念概括为从宾语a到 的态射Ω。这就是为什么Ω有时被称为真理对象。
In set theory, a characteristic function may be interpreted as defining a property of the elements of a set — a predicate that is true for some elements and false for others. The predicate isEven selects a subset of even numbers from the set of natural numbers. In a topos, we can generalize the idea of a predicate to be a morphism from object a to Ω. This is why Ω is sometimes called the truth object.
谓词是逻辑的构建块。拓扑包含研究逻辑所需的所有工具。它具有对应于逻辑连接(逻辑与)的乘积、对应于析取(逻辑或)的余积以及对应于含义的指数。除排中律(或等价的双重否定消除律)外,所有标准逻辑公理都在拓扑中成立。这就是为什么拓扑逻辑对应于构造性逻辑或直觉逻辑。
Predicates are the building blocks of logic. A topos contains all the necessary instrumentation to study logic. It has products that correspond to logical conjunctions (logical and), coproducts for disjunctions (logical or), and exponentials for implications. All standard axioms of logic hold in a topos except for the law of excluded middle (or, equivalently, double negation elimination). That’s why the logic of a topos corresponds to constructive or intuitionistic logic.
直觉逻辑一直在稳步发展,并从计算机科学中获得了意想不到的支持。排除中间的经典概念基于这样的信念:存在绝对真理:任何陈述要么是真,要么是假,或者如古罗马人所说,tertium non datur(没有第三种选择)。但我们知道某件事是真是假的唯一方法是证明或反驳它。证明是一个过程、一种计算——我们知道计算需要时间和资源。在某些情况下,它们可能永远不会终止。如果我们不能在有限的时间内证明一个陈述是正确的,那么声称它是正确的是没有意义的。拓扑及其更细致的真值对象为有趣的逻辑建模提供了更通用的框架。
Intuitionistic logic has been steadily gaining ground, finding unexpected support from computer science. The classical notion of excluded middle is based on the belief that there is absolute truth: Any statement is either true or false or, as Ancient Romans would say, tertium non datur (there is no third option). But the only way we can know whether something is true or false is if we can prove or disprove it. A proof is a process, a computation — and we know that computations take time and resources. In some cases, they may never terminate. It doesn’t make sense to claim that a statement is true if we cannot prove it in finite amount of time. A topos with its more nuanced truth object provides a more general framework for modeling interesting logics.
f的回拉的函数必须是单射的。truef that is the pullback of true along the characteristic function must be injective.如今,谈论函数式编程就不能不提到 monad。但在另一个宇宙中,尤金尼奥·莫吉(Eugenio Moggi)偶然将注意力转向了劳维尔理论而不是单子。让我们探索那个宇宙。
Nowadays you can’t talk about functional programming without mentioning monads. But there is an alternative universe in which, by chance, Eugenio Moggi turned his attention to Lawvere theories rather than monads. Let’s explore that universe.
在不同抽象层次上描述代数的方法有很多种。我们试图找到一种通用语言来描述幺半群、群或环等事物。在最简单的层面上,所有这些构造都定义了对集合元素的操作,以及这些操作必须满足的一些法则。例如,幺半群可以根据关联的二元运算来定义。我们还有单位元素和单位定律。但只要发挥一点想象力,我们就可以将单位元素转换为空操作——一种不带参数并返回集合中特殊元素的操作。如果我们想讨论组,我们添加一个一元运算符,它接受一个元素并返回其逆元。有相应的左逆定律和右逆定律与之相伴。环定义了两个二元运算符以及更多定律。等等。
There are many ways of describing algebras at various levels of abstraction. We try to find a general language to describe things like monoids, groups, or rings. At the simplest level, all these constructions define operations on elements of a set, plus some laws that must be satisfied by these operations. For instance, a monoid can be defined in terms of a binary operation that is associative. We also have a unit element and unit laws. But with a little bit of imagination we can turn the unit element to a nullary operation — an operation that takes no arguments and returns a special element of the set. If we want to talk about groups, we add a unary operator that takes an element and returns its inverse. There are corresponding left and right inverse laws to go with it. A ring defines two binary operators plus some more laws. And so on.
总体而言,代数是由一组针对不同 n 值的 n 元运算和一组等式恒等式定义的。这些身份都是普遍量化的。对于三个元素的所有可能组合,必须满足结合性方程,依此类推。
The big picture is that an algebra is defined by a set of n-ary operations for various values of n, and a set of equational identities. These identities are all universally quantified. The associativity equation must be satisfied for all possible combinations of three elements, and so on.
顺便说一句,这从考虑中消除了字段,原因很简单,零(相对于加法的单位)相对于乘法没有逆元。场的反演定律无法普遍量化。
Incidentally, this eliminates fields from consideration, for the simple reason that zero (unit with respect to addition) has no inverse with respect to multiplication. The inverse law for a field can’t be universally quantified.
如果我们用态射替换运算(函数),则通用代数的定义可以扩展到Set以外的范畴。a我们选择一个对象(称为通用对象)而不是集合。一元运算只是 的自同态a。但是其他 arities 呢(arity是给定操作的参数数量)?二元运算(arity 2)可以定义为从乘积a×a回到 的态射a。a一般的 n 元运算是to的 n 次方的态射a:
This definition of a universal algebra can be extended to categories other than Set, if we replace operations (functions) with morphisms. Instead of a set, we select an object a (called a generic object). A unary operation is just an endomorphism of a. But what about other arities (arity is the number of arguments for a given operation)? A binary operation (arity 2) can be defined as a morphism from the product a×a back to a. A general n-ary operation is a morphism from the n-th power of a to a:
αn :: an -> aαn :: an -> a
零运算是来自终端对象的态射( 的零次方a)。因此,为了定义任何代数,我们所需要的只是一个范畴,其对象是一个特殊对象的幂a。特定代数被编码在该范畴的 hom-sets 中。简而言之,这就是劳维尔理论。
A nullary operation is a morphism from the terminal object (the zeroth power of a). So all we need in order to define any algebra is a category whose objects are powers of one special object a. The specific algebra is encoded in the hom-sets of this category. This is a Lawvere theory in a nutshell.
Lawvere 理论的推导经历了许多步骤,因此路线图如下:
The derivation of Lawvere theories goes through many steps, so here’s the roadmap:
M:范畴 中的对象Mod(Law, Set)。M of a Lawvere category: an object in the category Mod(Law, Set).所有劳维尔理论都有一个共同的支柱。劳维尔理论中的所有物体都是由一个物体使用乘积(实际上只是幂)生成的。但我们如何在一般范畴中定义这些产品呢?事实证明,我们可以使用更简单范畴的映射来定义产品。事实上,这个更简单的范畴可以定义余积而不是乘积,我们将使用逆变函子将它们嵌入到我们的目标范畴中。逆变函子将余积转换为乘积,将注入转换为投影。
All Lawvere theories share a common backbone. All objects in a Lawvere theory are generated from just one object using products (really, just powers). But how do we define these products in a general category? It turns out that we can define products using a mapping from a simpler category. In fact this simpler category may define coproducts instead of products, and we’ll use a contravariant functor to embed them in our target category. A contravariant functor turns coproducts into products and injections to projections.
Lawvere 范畴的主干的自然选择是有限集范畴FinSet。它包含空集0、单元素集1、二元素集2等。此范畴中的所有对象都可以使用余积从单例集中生成(将空集视为无效余积的特殊情况)。例如,一个二元素集是两个单例 的总和,2 = 1 + 1如 Haskell 中所示:
The natural choice for the backbone of a Lawvere category is the category of finite sets, FinSet. It contains the empty set 0, a singleton set 1, a two-element set 2, and so on. All objects in this category can be generated from the singleton set using coproducts (treating the empty set as a special case of a nullary coproduct). For instance, a two-element set is a sum of two singletons, 2 = 1 + 1, as expressed in Haskell:
type Two = Either () ()type Two = Either () ()
然而,尽管人们很自然地认为只有一个空集,但可能存在许多不同的单例集。特别是,集合1 + 0不同于集合0 + 1,并且不同于1——尽管它们都是同构的。集合范畴中的余积不具有结合性。我们可以通过构建一个识别所有同构集的范畴来解决这种情况。这样的范畴称为骨架。换句话说,任何 Lawvere 理论的支柱都是FinSet的骨架F。该范畴中的对象可以用与FinSet中的元素计数相对应的自然数(包括零)来标识。余积起到加法的作用。F中的态射对应于有限集之间的函数。例如,从 到 存在唯一的态射(空集是初始对象),从 到 没有态射(除了),从到 的n 个态射(注入),从到 的一个态射,等等。这里,表示F中的一个对象,对应于FinSet中已通过同构识别的所有n元素集。0nn00->01nn1n
However, even though it’s natural to think that there’s only one empty set, there may be many distinct singleton sets. In particular, the set 1 + 0 is different from the set 0 + 1, and different from 1 — even though they are all isomorphic. The coproduct in the category of sets is not associative. We can remedy that situation by building a category that identifies all isomorphic sets. Such a category is called a skeleton. In other words, the backbone of any Lawvere theory is the skeleton F of FinSet. The objects in this category can be identified with natural numbers (including zero) that correspond to the element count in FinSet. Coproduct plays the role of addition. Morphisms in F correspond to functions between finite sets. For instance, there is a unique morphism from 0 to n (empty set being the initial object), no morphisms from n to 0 (except 0->0), n morphisms from 1 to n (the injections), one morphism from n to 1, and so on. Here, n denotes an object in F corresponding to all n-element sets in FinSet that have been identified through isomorphims.
使用范畴F,我们可以正式地将Lawvere 理论定义为配备特殊函子的范畴L
Using the category F we can formally define a Lawvere theory as a category L equipped with a special functor
IL :: Fop -> LIL :: Fop -> L
该函子必须是对象上的双射,并且必须保留有限乘积(F op中的乘积与F中的余积相同):
This functor must be a bijection on objects and it must preserve finite products (products in Fop are the same as coproducts in F):
IL (m × n) = IL m × IL nIL (m × n) = IL m × IL n
有时您可能会看到这个函子被表征为对象上的恒等性,这意味着F和L中的对象是相同的。因此,我们将为它们使用相同的名称 - 我们将用自然数表示它们。请记住,F中的对象与集合不同(它们是同构集合的类)。
You may sometimes see this functor characterized as identity-on-objects, which means that the objects in F and L are the same. We will therefore use the same names for them — we’ll denote them by natural numbers. Keep in mind though that objects in F are not the same as sets (they are classes of isomorphic sets).
一般来说,L中的 hom-set比F op中的 hom-set 更丰富。它们可能包含与FinSet中的函数对应的态射以外的态射(后者有时称为基本乘积运算)。劳维尔理论的方程定律被编码在这些态射中。
The hom-sets in L are, in general, richer than those in Fop. They may contain morphisms other than the ones corresponding to functions in FinSet (the latter are sometimes called basic product operations). Equational laws of a Lawvere theory are encoded in those morphisms.
关键的观察是F1中的单例集映射到我们也在L中调用的某个对象,并且L中的所有其他对象自动都是该对象的幂。例如, F中的二元素集是余积,因此它必须映射到L中的乘积(或)。从这个意义上说,范畴F 的行为类似于L的对数。121+11×112
The key observation is that the singleton set 1 in F is mapped to some object that we also call 1 in L, and all the other objects in L are automatically powers of this object. For instance, the two-element set 2 in F is the coproduct 1+1, so it must be mapped to a product 1×1 (or 12) in L. In this sense, the category F behaves like the logarithm of L.
在L的态射中,我们有那些由函子IL从F转移的态射。它们在L中发挥结构作用。特别是联产品注入ik成为产品预测pk。一个有用的直觉是想象一下投影:
Among morphisms in L we have those transferred by the functor IL from F. They play structural role in L. In particular coproduct injections ik become product projections pk. A useful intuition is to imagine the projection:
pk :: 1n -> 1pk :: 1n -> 1
作为 n 个变量的函数的原型,该函数忽略除第 k 个变量之外的所有变量。相反, Fn->1中的常数态射成为L中的对角态射。它们对应于变量的重复。1->1n
as the prototype for a function of n variables that ignores all but the k’th variable. Conversely, constant morphisms n->1 in F become diagonal morphisms 1->1n in L. They correspond to duplication of variables.
L中有趣的态射是定义除投影之外的 n 元运算的态射。正是这些态射将一种劳维尔理论与另一种理论区分开来。这些是定义代数的乘法、加法、单位元素的选择等等。但为了使L成为一个完整的范畴,我们还需要复合运算n->m(或者等效地,1n -> 1m)。由于该范畴的结构简单,因此它们是该类型的更简单态射的产物n->1。这是以下陈述的概括:返回乘积的函数是函数的乘积(或者,正如我们之前所见,hom 函子是连续的)。
The interesting morphisms in L are the ones that define n-ary operations other than projections. It’s those morphisms that distinguish one Lawvere theory from another. These are the multiplications, the additions, the selections of unit elements, and so on, that define the algebra. But to make L a full category, we also need compound operations n->m (or, equivalently, 1n -> 1m). Because of the simple structure of the category, they turn out to be products of simpler morphisms of the type n->1. This is a generalization of the statement that a function that returns a product is a product of functions (or, as we’ve seen earlier, that the hom-functor is continuous).
Lawvere 理论 L 基于F op,它继承了定义乘积的“无聊”态射。它添加了描述 n 元运算的“有趣”态射(虚线箭头)。
Lawvere theory L is based on Fop, from which it inherits the “boring” morphisms that define the products. It adds the “interesting” morphisms that describe the n-ary operations (dotted arrows).
Lavwere 理论形成了一个范畴律,其中态射是保留有限乘积并与函子交换的函子I。给定两个这样的理论(L, IL)和(L', I'L'),它们之间的态射是一个函子,F :: L -> L'使得:
Lavwere theories form a category Law, in which morphisms are functors that preserve finite products and commute with the functors I. Given two such theories, (L, IL) and (L', I'L'), a morphism between them is a functor F :: L -> L' such that:
F (m × n) = F m × F n
F ∘ IL = I'L'F (m × n) = F m × F n
F ∘ IL = I'L'
劳维尔理论之间的态射封装了用一种理论解释另一种理论的思想。例如,如果我们忽略逆,群乘法可能会被解释为幺半群乘法。
Morphisms between Lawvere theories encapsulate the idea of the interpretation of one theory inside another. For instance, group multiplication may be interpreted as monoid multiplication if we ignore inverses.
Lawvere 范畴最简单的例子是F op本身(对应于 的恒等函子的选择IL)。这种没有运算或定律的劳维尔理论恰好是法律的最初对象。
The simplest trivial example of a Lawvere category is Fop itself (corresponding to the choice of the identity functor for IL). This Lawvere theory that has no operations or laws happens to be the initial object in Law.
在这一点上,提出一个劳维尔理论的重要例子将非常有帮助,但如果不先了解模型是什么,就很难解释它。
At this point it would be very helpful to present a non-trivial example of a Lawvere theory, but it would be hard to explain it without first understanding what models are.
理解劳维尔理论的关键是认识到这样一种理论概括了许多共享相同结构的个体代数。例如,Lawvere 幺半群理论描述了幺半群的本质。它必须对所有幺半群都有效。一个特定的幺半群成为这种理论的模型。模型被定义为从 Lawvere 理论L到集合范畴Set的函子。(Lawvere 理论有使用其他范畴作为模型的概括,但这里我只关注Set 。)由于L的结构很大程度上取决于乘积,因此我们要求这样的函子保留有限乘积。L的模型,也称为 Lawvere 理论L 的代数,因此由函子定义:
The key to understand Lawvere theories is to realize that one such theory generalizes a lot of individual algebras that share the same structure. For instance, the Lawvere theory of monoids describes the essence of being a monoid. It must be valid for all monoids. A particular monoid becomes a model of such a theory. A model is defined as a functor from the Lawvere theory L to the category of sets Set. (There are generalizations of Lawvere theories that use other categories for models but here I’ll just concentrate on Set.) Since the structure of L depends heavily on products, we require that such a functor preserve finite products. A model of L, also called the algebra over the Lawvere theory L, is therefore defined by a functor:
M :: L -> Set
M (a × b) ≅ M a × M bM :: L -> Set
M (a × b) ≅ M a × M b
请注意,我们只要求产品的保存达到同构。这非常重要,因为严格保存产品会消除最有趣的理论。
Notice that we require the preservation of products only up to isomorphism. This is very important, because strict preservation of products would eliminate most interesting theories.
通过模型保存乘积意味着SetM中的图像是由该集合的幂生成的集合序列——来自L的对象的图像。我们称这个集合为。(这个集合有时称为 排序,这样的代数称为单排序。存在 Lawvere 理论到多排序代数的推广。)特别是,来自L的二元运算映射到函数:M 11a
The preservation of products by models means that the image of M in Set is a sequence of sets generated by powers of the set M 1 — the image of the object 1 from L. Let’s call this set a. (This set is sometimes called a sort, and such algebra is called single-sorted. There exist generalizations of Lawvere theories to multi-sorted algebras.) In particular, binary operations from L are mapped to functions:
a × a -> aa × a -> a
与任何函子一样, L中的多个态射可能会折叠为Set中的同一函数。
As with any functor, it’s possible that multiple morphisms in L are collapsed to the same function in Set.
顺便说一句,所有定律都是普遍量化的等式,这一事实意味着每个劳维尔理论都有一个平凡的模型:一个将所有对象映射到单个集合的常数函子,并将所有态射映射到其上的恒等函数。
Incidentally, the fact that all laws are universally quantified equalities means that every Lawvere theory has a trivial model: a constant functor mapping all objects to a single set, and all morphisms to the identity function on it.
L形式的一般态射m -> n被映射到一个函数:
A general morphism in L of the form m -> n is mapped to a function:
am -> anam -> an
如果我们有两个不同的模型M和N,它们之间的自然转换是由 索引的函数族n:
If we have two different models, M and N, a natural transformation between them is a family of functions indexed by n:
μn :: M n -> N nμn :: M n -> N n
或者,等效地:
or, equivalently:
μn :: an -> bnμn :: an -> bn
哪里b = N 1。
where b = N 1.
请注意,自然性条件保证了 n 元运算的保存:
Notice that the naturality condition guarantees the preservation of n-ary operations:
N f ∘ μn = μ1 ∘ M fN f ∘ μn = μ1 ∘ M f
其中是Lf :: n -> 1中的 n 元运算。
where f :: n -> 1 is an n-ary operation in L.
定义模型的函子形成了一类模型,Mod(L, Set),具有作为态射的自然变换。
The functors that define models form a category of models, Mod(L, Set), with natural transformations as morphisms.
考虑平凡 Lawvere 范畴F op的模型。1这种模型完全由其在,处的值决定M 1。由于可以是任何集合,因此这些模型的数量与SetM 1中的集合的数量一样多。此外, (函子和之间的自然变换)中的每个态射都是由其在 处的分量唯一确定的。相反,每个函数都会引起两个模型和之间的自然转换。因此相当于Set。Mod(Fop, Set)MNM 1M 1 -> N 1MNMod(Fop, Set)
Consider a model for the trivial Lawvere category Fop. Such model is completely determined by its value at 1, M 1. Since M 1 can be any set, there are as many of these models as there are sets in Set. Moreover, every morphism in Mod(Fop, Set) (a natural transformation between functors M and N) is uniquely determined by its component at M 1. Conversely, every function M 1 -> N 1 induces a natural transformation between the two models M and N. Therefore Mod(Fop, Set) is equivalent to Set.
劳维尔理论最简单的例子描述了幺半群的结构。它是一个单一理论,提炼了所有可能的幺半群的结构,从某种意义上说,该理论的模型跨越了幺半群的整个Mon范畴。我们已经看到了一个通用构造,它表明每个幺半群都可以通过识别态射的子集从适当的自由幺半群获得。所以一个自由的幺半群已经概括了很多幺半群。然而,自由幺半群有无限多个。幺半群L Mon的劳维尔理论将所有这些理论结合在一个优雅的结构中。
The simplest nontrivial example of a Lawvere theory describes the structure of monoids. It is a single theory that distills the structure of all possible monoids, in the sense that the models of this theory span the whole category Mon of monoids. We’ve already seen a universal construction, which showed that every monoid can be obtained from an appropriate free monoid by identifying a subset of morphisms. So a single free monoid already generalizes a whole lot of monoids. There are, however, infinitely many free monoids. The Lawvere theory for monoids LMon combines all of them in one elegant construction.
每个幺半群必须有一个单位,因此L Monη中必须有一个特殊的态射,从到。请注意, F中不可能有相应的态射。这种态射将朝相反的方向发展,在FinSet中,将是从单例集到空集的函数。不存在这样的功能。0110
Every monoid must have a unit, so we have to have a special morphism η in LMon that goes from 0 to 1. Notice that there can be no corresponding morphism in F. Such morphism would go in the opposite direction, from 1 to 0 which, in FinSet, would be a function from the singleton set to the empty set. No such function exists.
接下来,考虑态射2->1( 的成员)LMon(2, 1),它必须包含所有二元运算的原型。在 中构建模型时Mod(LMon, Set),这些态射将被映射到笛卡尔积M 1 × M 1到 的函数M 1。换句话说,两个参数的函数。
Next, consider morphisms 2->1, members of LMon(2, 1), which must contain prototypes of all binary operations. When constructing models in Mod(LMon, Set), these morphisms will be mapped to functions from the cartesian product M 1 × M 1 to M 1. In other words, functions of two arguments.
问题是:仅使用幺半群运算符可以实现多少个两个参数的函数。我们将这两个参数称为a和b。有一个函数忽略两个参数并返回幺半群单位。然后有两个投影分别返回a和b。它们后面是返回ab、ba、aa、bb、aab、 等的函数……事实上,带有生成器 和 的自由幺半群中的元素数量与具有两个参数的此类函数一样a多b。请注意LMon(2, 1)必须包含所有这些态射,因为其中一个模型是自由幺半群。在自由幺半群中,它们对应于不同的函数。其他模型可能会将多个态射折叠LMon(2, 1)为单个函数,但自由幺半群则不然。
The question is: how many functions of two arguments can one implement using only the monoidal operator. Let’s call the two arguments a and b. There is one function that ignores both arguments and returns the monoidal unit. Then there are two projections that return a and b, respectively. They are followed by functions that return ab, ba, aa, bb, aab, and so on… In fact there are as many such functions of two arguments as there are elements in the free monoid with generators a and b. Notice that LMon(2, 1) must contain all those morphisms because one of the models is the free monoid. In a free monoid they correspond to distinct functions. Other models may collapse multiple morphisms in LMon(2, 1) down to a single function, but not the free monoid.
如果我们用 n 个生成器 来表示自由幺半群n*,我们就可以用Mon中的hom 集L(2, 1)来识别 hom 集,Mon 是幺半群的范畴。一般来说,我们选择是。换句话说,该范畴与自由幺半群的范畴相反。Mon(1*, 2*)LMon(m, n)Mon(n*, m*)LMon
If we denote the free monoid with n generators n*, we may identify the hom-set L(2, 1) with the hom-set Mon(1*, 2*) in Mon, the category of monoids. In general, we pick LMon(m, n) to be Mon(n*, m*). In other words, the category LMon is the opposite of the category of free monoids.
幺半群的 Lawvere 理论的模型范畴Mod(LMon, Set),等价于所有幺半群的范畴Mon。
The category of models of the Lawvere theory for monoids, Mod(LMon, Set), is equivalent to the category of all monoids, Mon.
您可能还记得,代数理论可以使用单子来描述——特别是单子的代数。那么劳维尔理论和单子之间存在联系也就不足为奇了。
As you may remember, algebraic theories can be described using monads — in particular algebras for monads. It should be no surprise then that there is a connection between Lawvere theories and monads.
首先,让我们看看劳维尔理论如何推导出单子。它通过健忘函子和自由函子之间的附加来实现这一点。健忘函子U为每个模型分配一个集合。该集合是通过评估L中Mod(L, Set)对象的函子 M 来给出的。1
First, let’s see how a Lawvere theory induces a monad. It does it through an adjunction between a forgetful functor and a free functor. The forgetful functor U assigns a set to each model. This set is given by evaluating the functor M from Mod(L, Set) at the object 1 in L.
另一种推导方法U是利用F op是Law中的初始对象这一事实。这意味着,对于任何劳维尔理论L,都有一个唯一的函子Fop -> L。该函子在模型上导出相反的函子(因为模型是从理论到集合的函子):
Another way of deriving U is by exploiting the fact that Fop is the initial object in Law. It meanst that, for any Lawvere theory L, there is a unique functor Fop -> L. This functor induces the opposite functor on models (since models are functors from theories to sets):
Mod(L, Set) -> Mod(Fop, Set)Mod(L, Set) -> Mod(Fop, Set)
但是,正如我们所讨论的, F op的模型范畴相当于Set,因此我们得到了健忘函子:
But, as we discussed, the category of models of Fop is equivalent to Set, so we get the forgetful functor:
U :: Mod(L, Set) -> SetU :: Mod(L, Set) -> Set
可以证明,这样定义的U总是有一个左伴随,即自由函子F。
It can be shown that so defined U always has a left adjoint, the free functor F.
对于有限集,这很容易看出。自由函子F产生自由代数。自由代数是Mod(L, Set)由一组有限的生成器生成的特定模型n。我们可以将其实现F为可表示函子:
This is easily seen for finite sets. The free functor F produces free algebras. A free algebra is a particular model in Mod(L, Set) that is generated from a finite set of generators n. We can implement F as the representable functor:
L(n, -) :: L -> SetL(n, -) :: L -> Set
为了证明它确实是免费的,我们所要做的就是证明它是健忘函子的左伴随:
To show that it’s indeed free, all we have to do is to prove that it’s a left adjoint to the forgetful functor:
Mod(L(n, -), M) ≅ Set(n, U(M))Mod(L(n, -), M) ≅ Set(n, U(M))
让我们简化右侧:
Let’s simplify the right hand side:
Set(n, U(M)) ≅ Set(n, M 1) ≅ (M 1)n ≅ M nSet(n, U(M)) ≅ Set(n, M 1) ≅ (M 1)n ≅ M n
(我利用了一组态射与指数同构的事实,在这种情况下,指数只是迭代积。)附加是米田引理的结果:
(I used the fact that a set of morphisms is isomorphic to the exponential which, in this case, is just the iterated product.) The adjunction is the result of the Yoneda lemma:
[L, Set](L(n, -), M) ≅ M n[L, Set](L(n, -), M) ≅ M n
健忘函子和自由函子一起定义了Set上的一个单子 。因此,每一个劳维尔理论都会产生一个单子。T = U∘F
Together, the forgetful and the free functor define a monad T = U∘F on Set. Thus every Lawvere theory generates a monad.
事实证明,这个单子的代数范畴相当于模型范畴。
It turns out that the category of algebras for this monad is equivalent to the category of models.
您可能还记得,单子代数定义了计算使用单子形成的表达式的方法。Lawvere 理论定义了可用于生成表达式的 n 元运算。模型提供了评估这些表达式的方法。
You may recall that monad algebras define ways to evaluate expressions that are formed using monads. A Lawvere theory defines n-ary operations that can be used to generate expressions. Models provide means to evaluate these expressions.
不过,单子和劳维尔理论之间的联系并不是双向的。只有有限单子才能引出劳维尔理论。有限单子基于有限函子。集合上的有限函子完全由它在有限集合上的作用决定。a可以使用以下 coend 来评估它对任意集合的作用:
The connection between monads and Lawvere theories doesn’t go both ways, though. Only finitary monads lead to Lawvere thories. A finitary monad is based on a finitary functor. A finitary functor on Set is fully determined by its action on finite sets. Its action on an arbitrary set a can be evaluated using the following coend:
F a = ∫ n an × (F n)F a = ∫ n an × (F n)
由于余数概括了余积或和,因此该公式是幂级数展开式的概括。或者我们可以使用函子是广义容器的直觉。在这种情况下,s 的有限容器a可以被描述为形状和内容的总和。这里,F n是一组用于存储 n 个元素的形状,内容是一个 n 元组元素,其本身是 的元素an。例如,列表(作为函子)是有限的,每个元数都有一种形状。每棵树有更多的形状,依此类推。
Since the coend generalizes a coproduct, or a sum, this formula is a generalization of a power series expansion. Or we can use the intuition that a functor is a generalized container. In that case a finitary container of as can be described as a sum of shapes and contents. Here, F n is a set of shapes for storing n elements, and the contents is an n-tuple of elements, itself an element of an. For instance, a list (as a functor) is finitary, with one shape for every arity. A tree has more shapes per arity, and so on.
首先,从 Lawvere 理论生成的所有单子都是有限的,它们可以表示为共数:
First off, all monads that are generated from Lawvere theories are finitary and they can be expressed as coends:
TL a = ∫ n an × L(n, 1)TL a = ∫ n an × L(n, 1)
相反,给定SetT上的任何有限单子,我们可以构造一个 Lawvere 理论。我们首先为 构建一个 Kleisli 范畴。您可能还记得,从到 的Kleisli 范畴中的态射是由基础范畴中的态射给出的:Tab
Conversely, given any finitary monad T on Set, we can construct a Lawvere theory. We start by constructing a Kleisli category for T. As you may remember, a morphism in a Kleisli category from a to b is given by a morphism in the underlying category:
a -> T ba -> T b
当限制为有限集时,这变成:
When restricted to finite sets, this becomes:
m -> T nm -> T n
与此 Kleisli 范畴相反的范畴Kl T op仅限于有限集,是所讨论的劳维尔理论。特别地,描述LL(n, 1)中的 n 元运算的hom-set由 hom-set 给出。KlT(1, n)
The category opposite to this Kleisli category, KlTop, restricted to finite sets, is the Lawvere theory in question. In particular, the hom-set L(n, 1) that describes n-ary operations in L is given by the hom-set KlT(1, n).
事实证明,我们在编程中遇到的大多数单子都是有限的,但延续单子是一个明显的例外。将劳维尔理论的概念扩展到有限运算之外是可能的。
It turns out that most monads that we encounter in programming are finitary, with the notable exception of the continuation monad. It is possible to to extend the notion of Lawvere theory beyond finitary operations.
让我们更详细地探讨 coend 公式。
Let’s explore the coend formula in more detail.
TL a = ∫ n an × L(n, 1)TL a = ∫ n an × L(n, 1)
首先,这个余数取代了FP中的一个函子,定义为:
To begin with, this coend is taken over a profunctor P in F defined as:
P n m = an × L(m, 1)P n m = an × L(m, 1)
这个函子在第一个参数 中是逆变的n。考虑一下它是如何提升态射的。FinSet中的态射是有限集的映射f :: m -> n。这种映射描述了从 n 元素集中选择 m 个元素(允许重复)。可以将其提升为 的幂映射a,即(注意方向):
This profunctor is contravariant in the first argument, n. Consider how it lifts morphisms. A morphism in FinSet is a mapping of finite sets f :: m -> n. Such a mapping describes a selection of m elements from an n-element set (repetitions are allowed). It can be lifted to the mapping of powers of a, namely (notice the direction):
an -> aman -> am
提升只是从 n 个元素的元组中选择 m 个元素(a1, a2,...an)(可能有重复)。
The lifting simply selects m elements from a tuple of n elements (a1, a2,...an) (possibly with repetitions).
例如,让我们从 n 元素集中fk :: 1 -> n选择第一个元素。k它提升为一个函数,该函数接受 n 元组的元素a并返回第k一个。
For instance, let’s take fk :: 1 -> n — a selection of the kth element from an n-element set. It lifts to a function that takes a n-tuple of elements of a and returns the kth one.
或者让我们采用f :: m -> 1一个常量函数,将所有 m 个元素映射到 1。它的提升是一个函数,它接受单个元素a并将其复制 m 次:
Or let’s take f :: m -> 1 — a constant function that maps all m elements to one. Its lifting is a function that takes a single element of a and duplicates it m times:
λx -> (x, x, ... x)λx -> (x, x, ... x)
您可能会注意到,所讨论的函子在第二个参数中是协变的这一点并不是很明显。hom 函子L(m, 1)实际上是 中的逆变m。但是,我们不在范畴L中取余数,而是在范畴F中取余数。coend 变量n遍历有限集(或此类的骨架)。范畴L包含F的相反数,因此Fm -> n中的态射是L中的成员(嵌入由函子 给出)。L(n, m)IL
You might notice that it’s not immediately obvious that the profunctor in question is covariant in the second argument. The hom-functor L(m, 1) is actually contravariant in m. However, we are taking the coend not in the category L but in the category F. The coend variable n goes over finite sets (or the skeletons of such). The category L contains the opposite of F, so a morphism m -> n in F is a member of L(n, m) in L (the embedding is given by the functor IL).
让我们检查一下作为从F到Set 的L(m, 1)函子的函子性。我们想要提升一个函数,所以我们的目标是实现一个从到 的函数。对应于函数,L from to中有一个态射(注意方向)。将这个态射 预组合给我们一个 的子集。f :: m -> nL(m, 1)L(n, 1)fnmL(m, 1)L(n, 1)
Let’s check the functoriality of L(m, 1) as a functor from F to Set. We want to lift a function f :: m -> n, so our goal is to implement a function from L(m, 1) to L(n, 1). Corresponding to the function f there is a morphism in L from n to m (notice the direction). Precomposing this morphism with L(m, 1) gives us a subset of L(n, 1).
请注意,通过提升一个函数,1->n我们可以从L(1, 1)到L(n, 1)。稍后我们将使用这个事实。
Notice that, by lifting a function 1->n we can go from L(1, 1) to L(n, 1). We’ll use this fact later on.
an逆变函子和协变函子的乘积L(m, 1)是泛函子Fop×F->Set。请记住,余数可以定义为余函子的所有对角成员的余积(不相交和),其中标识了一些元素。这些标识对应于边缘条件。
The product of a contravariant functor an and a covariant functor L(m, 1) is a profunctor Fop×F->Set. Remember that a coend can be defined as a coproduct (disjoint sum) of all the diagonal members of a profunctor, in which some elements are identified. The identifications correspond to cowedge conditions.
an × L(n, 1)在这里,共端开始于所有 s 上的集合的不相交和n。可以通过将coend 表示为 coequilizer 来生成标识。我们从非对角项 开始an × L(m, 1)。为了得到对角线,我们可以对乘积的f :: m -> n第一个或第二个分量应用态射。然后识别这两个结果。
Here, the coend starts as the disjoint sum of sets an × L(n, 1) over all ns. The identifications can be generated by expressing the coend as a coequilizer. We start with an off-diagonal term an × L(m, 1). To get to the diagonal, we can apply a morphism f :: m -> n either to the first or the second component of the product. The two results are then identified.
f :: 1 -> n我之前已经展示过这两个转换的结果提升:
I have shown before that the lifting of f :: 1 -> n results in these two transformations:
an -> aan -> a
和:
and:
L(1, 1) -> L(n, 1)L(1, 1) -> L(n, 1)
an × L(1, 1)因此,我们可以从以下两个方面出发:
Therefore, starting from an × L(1, 1) we can reach both:
a × L(1, 1)a × L(1, 1)
当我们举起<f, id>并且:
when we lift <f, id> and:
an × L(n, 1)an × L(n, 1)
当我们抬起<id, f>。然而,这并不意味着 的所有元素都an × L(n, 1)可以用 来标识a × L(1, 1)。这是因为并非 的所有元素都L(n, 1)可以从 到达L(1, 1)。请记住,我们只能从F中提升态射。L中的非平凡 n 元运算不能通过提升态射 来构造f :: 1 -> n。
when we lift <id, f>. This doesn’t mean, however, that all elements of an × L(n, 1) can be identified with a × L(1, 1). That’s because not all elements of L(n, 1) can be reached from L(1, 1). Remember that we can only lift morphisms from F. A non-trivial n-ary operation in L cannot be constructed by lifting a morphism f :: 1 -> n.
换句话说,我们只能识别出共数公式中可以通过应用基本态射L(n, 1)得出的所有加数。L(1, 1)它们都等价于a × L(1, 1). 基本态射是F中态射的图像。
In other words, we can only identify all addends in the coend formula for which L(n, 1) can be reached from L(1, 1) through the application of basic morphisms. They are all equivalent to a × L(1, 1). Basic morphisms are the ones that are images of morphisms in F.
让我们看看在 Lawvere 理论最简单的情况(F op本身)中这是如何运作的。在这样的理论中,每个都L(n, 1)可以从 到达L(1, 1)。这是因为L(1, 1)是 一个仅包含恒等态射的单例,并且L(n, 1)仅包含与F1->n中的注入相对应的态射,这是基本态射。因此,余积中的所有加数都是等价的,我们得到:
Let’s see how this works in the simplest case of the Lawvere theory, the Fop itself. In such a theory, every L(n, 1) can be reached from L(1, 1). This is because L(1, 1) is a singleton containing just the identity morphism, and L(n, 1) only contains morphisms corresponding to injections 1->n in F, which are basic morphisms. Therefore all the addends in the coproduct are equivalent and we get:
T a = a × L(1, 1) = aT a = a × L(1, 1) = a
这是恒等单子。
which is the identity monad.
由于单子和劳维尔理论之间存在如此紧密的联系,因此很自然地会问劳维尔理论是否可以在编程中用作单子的替代品。monad 的主要问题是它们不能很好地组合。构建 monad 变压器没有通用的方法。Lawvere 理论在这方面具有优势:它们可以使用余积和张量积组成。另一方面,只有有限单子才能轻松转换为劳维尔理论。这里的异常值是延续单子。该领域正在进行研究(参见参考书目)。
Since there is such a strong connection between monads and Lawvere theories, it’s natural to ask the question if Lawvere theories could be used in programming as an alternative to monads. The major problem with monads is that they don’t compose nicely. There is no generic recipe for building monad transformers. Lawvere theories have an advantage in this area: they can be composed using coproducts and tensor products. On the other hand, only finitary monads can be easily converted to Lawvere theories. The outlier here is the continuation monad. There is ongoing research in this area (see bibliography).
为了让您了解如何使用 Lawvere 理论来描述副作用,我将讨论传统上使用 monad 实现的简单异常情况Maybe。
To give you a taste of how a Lawvere theory can be used to describe side effects, I’ll discuss the simple case of exceptions that are traditionally implemented using the Maybe monad.
单子Maybe是由 Lawvere 理论通过单个零运算生成的0->1。该理论的模型是一个映射1到某个集合 的函子a,并将零运算映射到一个函数:
The Maybe monad is generated by the Lawvere theory with a single nullary operation 0->1. A model of this theory is a functor that maps 1 to some set a, and maps the nullary operation to a function:
raise :: () -> araise :: () -> a
我们可以Maybe使用 coend 公式恢复 monad。让我们考虑一下添加无效操作对 hom-sets 有何作用L(n, 1)。除了创建一个新的( F opL(0, 1)中没有)之外,它还向 中添加了新的态射。这些是将类型的态射与我们的组合的结果。这些贡献都在 coend 公式中标识,因为它们可以从以下位置获得:L(n, 1)n->00->1a0 × L(0, 1)
We can recover the Maybe monad using the coend formula. Let’s consider what the addition of the nullary operation does to the hom-sets L(n, 1). Besides creating a new L(0, 1) (which is absent from Fop), it also adds new morphisms to L(n, 1). These are the results of composing morphism of the type n->0 with our 0->1. Such contributions are all identified with a0 × L(0, 1) in the coend formula, because they can be obtained from:
an × L(0, 1)an × L(0, 1)
0->n通过两种不同的方式提升。
by lifting 0->n in two different ways.
coend 减少为:
The coend reduces to:
TL a = a0 + a1TL a = a0 + a1
或者,使用 Haskell 表示法:
or, using Haskell notation:
type Maybe a = Either () atype Maybe a = Either () a
这相当于:
which is equivalent to:
data Maybe a = Nothing | Just adata Maybe a = Nothing | Just a
请注意,这个 Lawvere 理论仅支持异常的引发,而不支持异常的处理。
Notice that this Lawvere theory only supports the raising of exceptions, not their handling.
我感谢 Gershom Bazerman 提供的许多有用的评论。
I’m grateful to Gershom Bazerman for many useful comments.
范畴论的书没有一个好的结尾。总是有更多东西需要学习。范畴论是一个广阔的学科。与此同时,很明显相同的主题、概念和模式不断出现。有一种说法是,所有概念都是 Kan 扩展,事实上,您可以使用 Kan 扩展来导出极限、余极限、附加词、单子、米田引理等等。范畴的概念本身出现在所有抽象层次上,幺半群和单子的概念也是如此。哪一项是最基本的?事实证明,它们都是相互关联的,在永无休止的抽象循环中,一个导致另一个。我决定展示这些相互联系可能是结束这本书的好方法。
There is no good place to end a book on category theory. There’s always more to learn. Category theory is a vast subject. At the same time, it’s obvious that the same themes, concepts, and patterns keep showing up over and over again. There is a saying that all concepts are Kan extensions and, indeed, you can use Kan extensions to derive limits, colimits, adjunctions, monads, the Yoneda lemma, and much more. The notion of a category itself arises at all levels of abstraction, and so does the concept of a monoid and a monad. Which one is the most basic? As it turns out they are all interrelated, one leading to another in a never-ending cycle of abstractions. I decided that showing these interconnections might be a good way to end this book.
范畴论最困难的方面之一是视角的不断切换。以集合范畴为例。我们习惯于用元素来定义集合。空集没有元素。单例集只有一个元素。两个集合的笛卡尔积是一组对,依此类推。但是,当谈论集合范畴时,我要求您忘记集合的内容,而专注于它们之间的态射(箭头)。你被允许时不时地窥探一下Set中的特定通用结构是如何用元素描述的。最终对象是一个包含一个元素的集合,依此类推。但这些只是健全性检查。
One of the most difficult aspects of category theory is the constant switching of perspectives. Take the category of sets, for instance. We are used to defining sets in terms of elements. An empty set has no elements. A singleton set has one element. A cartesian product of two sets is a set of pairs, and so on. But when talking about the category Set I asked you to forget about the contents of sets and instead concentrate on morphisms (arrows) between them. You were allowed, from time to time, to peek under the covers to see what a particular universal construction in Set described in terms of elements. The terminal object turned out to be a set with one element, and so on. But these were just sanity checks.
函子被定义为范畴的映射。将映射视为范畴中的态射是很自然的。函子被证明是范畴范畴中的态射(小范畴,如果我们想避免有关大小的问题)。通过将函子视为箭头,我们就放弃了有关其对范畴内部(其对象和态射)的作用的信息,就像当我们将函数视为箭头时,我们放弃了有关函数对集合元素的作用的信息一样Set中的箭头。但任意两个范畴之间的函子也构成一个范畴。这次,您被要求将某个范畴中的箭头视为另一范畴中的对象。在函子范畴中,函子是对象,自然变换是态射。我们发现,同一个东西可以是一个范畴中的箭头,而另一个范畴中可以是一个对象。将物体视为名词、将箭头视为动词的天真的观点并不成立。
A functor is defined as a mapping of categories. It’s natural to consider a mapping as a morphism in a category. A functor turned out to be a morphism in the category of categories (small categories, if we want to avoid questions about size). By treating a functor as an arrow, we forfeit the information about its action on the internals of a category (its objects and morphisms), just like we forfeit the information about the action of a function on elements of a set when we treat it as an arrow in Set. But functors between any two categories also form a category. This time you are asked to consider something that was an arrow in one category to be an object in another. In a functor category functors are objects and natural transformations are morphisms. We have discovered that the same thing can be an arrow in one category and an object in another. The naive view of objects as nouns and arrows as verbs doesn’t hold.
我们可以尝试将它们合并为一个,而不是在两个视图之间切换。这就是我们如何得到2-范畴的概念,其中对象被称为0-细胞,态射被称为1-细胞,态射之间的态射被称为2-细胞。
Instead of switching between two views, we can try to merge them into one. This is how we get the concept of a 2-category, in which objects are called 0-cells, morphisms are 1-cells, and morphisms between morphisms are 2-cells.
0-细胞a、b;1-细胞f、g;和 2 细胞 α。
0-cells a, b; 1-cells f, g; and a 2-cell α.
范畴Cat的范畴就是一个直接的例子。我们将范畴视为 0 细胞,将函子视为 1 细胞,将自然变换视为 2 细胞。2 范畴定律告诉我们,任意两个 0 单元格之间的 1 单元格形成一个范畴(换句话说,C(a, b)是 hom 范畴而不是 hom 集)。这非常符合我们之前的断言,即任何两个范畴之间的函子形成一个函子范畴。
The category of categories Cat is an immediate example. We have categories as 0-cells, functors as 1-cells, and natural transformations as 2-cells. The laws of a 2-category tell us that 1-cells between any two 0-cells form a category (in other words, C(a, b) is a hom-category rather than a hom-set). This fits nicely with our earlier assertion that functors between any two categories form a functor category.
特别是,从任何 0 单元回到自身的 1 单元也形成一个范畴,即 hom 范畴C(a, a);但该范畴具有更多结构。的成员C(a, a)可以被视为C中的箭头或 中的对象C(a, a)。作为箭头,它们可以相互组合。但当我们将它们视为对象时,组合就变成了从一对对象到一个对象的映射。事实上,它看起来非常像一个乘积——准确地说是一个张量乘积。该张量积有一个单位:单位 1-cell。事实证明,在任何 2-范畴中,hom 范畴C(a, a)自动是一个幺半群范畴,其张量积定义为 1-cell 的组合。结合律和单位定律只是从相应的范畴定律中分离出来。
In particular, 1-cells from any 0-cell back to itself also form a category, the hom-category C(a, a); but that category has even more structure. Members of C(a, a) can be viewed as arrows in C or as objects in C(a, a). As arrows, they can be composed with each other. But when we look at them as objects, the composition becomes a mapping from a pair of objects to an object. In fact it looks very much like a product — a tensor product to be precise. This tensor product has a unit: the identity 1-cell. It turns out that, in any 2-category, a hom-category C(a, a) is automatically a monoidal category with the tensor product defined as composition of 1-cells. Associativity and unit laws simply fall out from the corresponding category laws.
让我们看看这在 2 类Cat的规范示例中意味着什么。hom 范畴Cat(a, a)是 上的内函子范畴a。Endofunctor 组合在其中起到了张量积的作用。恒等函子是与该乘积相关的单位。我们之前已经看到endofunctors形成了一个幺半群范畴(我们在单子的定义中使用了这个事实),但现在我们看到这是一个更普遍的现象:任何2-范畴中的endo-1-cells形成一个幺半群范畴。稍后当我们概括 monad 时我们会再次讨论它。
Let’s see what this means in our canonical example of a 2-category Cat. The hom-category Cat(a, a) is the category of endofunctors on a. Endofunctor composition plays the role of a tensor product in it. The identity functor is the unit with respect to this product. We’ve seen before that endofunctors form a monoidal category (we used this fact in the definition of a monad), but now we see that this is a more general phenomenon: endo-1-cells in any 2-category form a monoidal category. We’ll come back to it later when we generalize monads.
您可能还记得,在一般幺半群范畴中,我们并不坚持在鼻子上满足幺半群定律。通常满足单位定律和结合律直到同构就足够了。在 2 范畴中,幺半群定律C(a, a)遵循 1 单元的组成定律。这些定律是严格的,所以我们总是会得到严格的幺半群范畴。然而,也可以放宽这些法律。ida例如,我们可以说,恒等 1-cell与另一个 1-cell的组合 与f :: a -> b是同构的,而不是等于f。1 细胞的同构是使用 2 细胞定义的。换句话说,有一个 2 单元格:
You might recall that, in a general monoidal category, we did not insist on the monoid laws being satisfied on the nose. It was often enough for the unit laws and the associativity laws to be satisfied up to isomorphism. In a 2-category, monoidal laws in C(a, a) follow from composition laws for 1-cells. These laws are strict, so we will always get a strict monoidal category. It is, however, possible to relax these laws as well. We can say, for instance, that a composition of the identity 1-cell ida with another 1-cell, f :: a -> b, is isomorphic, rather than equal, to f. Isomorphism of 1-cells is defined using 2-cells. In other words, there is a 2-cell:
ρ :: f ∘ ida -> fρ :: f ∘ ida -> f
有一个逆元。
that has an inverse.
二类中的恒等律适用于同构(可逆的 2 单元 ρ)。
Identity law in a bicategory holds up to isomorphism (an invertible 2-cell ρ).
我们可以对左恒等律和结合律做同样的事情。这种宽松的二分类称为二分类(还有一些额外的一致性律,我将在这里省略)。
We can do the same for the left identity and associativity laws. This kind of relaxed 2-category is called a bicategory (there are some additional coherency laws, which I will omit here).
正如预期的那样,双范畴中的内切1细胞形成了具有非严格规律的一般幺半群范畴。
As expected, endo-1-cells in a bicategory form a general monoidal category with non-strict laws.
双范畴的一个有趣的例子是跨度范畴。两个对象之间的跨度a是b一个对象x和一对态射:
An interesting example of a bicategory is the category of spans. A span between two objects a and b is an object x and a pair of morphisms:
f :: x -> a
g :: x -> bf :: x -> a
g :: x -> b
您可能还记得我们在分类产品的定义中使用了跨度。在这里,我们希望将跨度视为二范畴中的 1 个单元格。第一步是定义跨度的组合。假设我们有一个相邻的跨度:
You might recall that we used spans in the definition of a categorical product. Here, we want to look at spans as 1-cells in a bicategory. The first step is to define a composition of spans. Suppose that we have an adjoining span:
f':: y -> b
g':: y -> cf':: y -> b
g':: y -> c
该作品将是第三个跨度,有一些顶点z。最自然的选择就是g沿线的回调f'。请记住,回调是z带有两个态射的对象:
The composition would be a third span, with some apex z. The most natural choice for it is the pullback of g along f'. Remember that a pullback is the object z together with two morphisms:
h :: z -> x
h':: z -> yh :: z -> x
h':: z -> y
这样:
such that:
g ∘ h = f' ∘ h'g ∘ h = f' ∘ h'
这是所有此类对象中普遍存在的。
which is universal among all such objects.
现在,让我们集中讨论集合范畴上的跨度。在这种情况下,回调只是(p, q)笛卡尔积中的一组对x × y,使得:
For now, let’s concentrate on spans over the category of sets. In that case, the pullback is just a set of pairs (p, q) from the cartesian product x × y such that:
g p = f' qg p = f' q
共享相同端点的两个跨度之间的态射被定义为h它们的顶点之间的态射,使得适当的三角形可交换。
A morphism between two spans that share the same endpoints is defined as a morphism h between their apices, such that the appropriate triangles commute.
总而言之,在二类Span中:0 单元是集合,1 单元是跨度,2 单元是跨度态射。恒等 1-cell 是一个简并跨度,其中所有三个对象都是相同的,并且两个态射都是恒等的。
To summarize, in the bicategory Span: 0-cells are sets, 1-cells are spans, 2-cells are span morphisms. An identity 1-cell is a degenerate span in which all three objects are the same, and the two morphisms are identities.
我们之前见过另一个二分类的例子:二分类Profunctors,其中 0-cells 是范畴,1-cells 是 profunctors,2-cells 是自然变换。助函子的组成由一个共同词给出。
We’ve seen another example of a bicategory before: the bicategory Prof of profunctors, where 0-cells are categories, 1-cells are profunctors, and 2-cells are natural transformations. The composition of profunctors was given by a coend.
到目前为止,您应该非常熟悉作为内函子范畴中的幺半群的单子的定义。让我们带着新的理解重新审视这个定义,即 endofunctors 范畴只是双范畴Cat中的 end-1-cells 的一个小 hom-category 。我们知道它是一个幺半群范畴:张量积来自于内函子的组合。幺半群被定义为幺半群范畴中的一个对象——在这里它将是一个内函子T——以及两个态射。内函子之间的态射是自然变换。一种态射将幺半群单位(恒等内函子)映射到T:
By now you should be pretty familiar with the definition of a monad as a monoid in the category of endofunctors. Let’s revisit this definition with the new understanding that the category of endofunctors is just one small hom-category of endo-1-cells in the bicategory Cat. We know it’s a monoidal category: the tensor product comes from the composition of endofunctors. A monoid is defined as an object in a monoidal category — here it will be an endofunctor T — together with two morphisms. Morphisms between endofunctors are natural transformations. One morphism maps the monoidal unit — the identity endofunctor — to T:
η :: I -> Tη :: I -> T
T ⊗ T第二个态射映射到的张量积T。张量积由内函子组成给出,因此我们得到:
The second morphism maps the tensor product of T ⊗ T to T. The tensor product is given by endofunctor composition, so we get:
μ :: T ∘ T -> Tμ :: T ∘ T -> T
我们将这些视为定义单子的两个操作(它们在 Haskell 中称为return和join),并且我们知道幺半群定律转向单子定律。
We recognize these as the two operations defining a monad (they are called return and join in Haskell), and we know that monoid laws turn to monad laws.
现在让我们从这个定义中删除所有提及的endofunctors。我们从一个二范畴开始,并在其中C选择一个 0 单元格。a正如我们之前所看到的,hom 范畴C(a, a)是一个幺半群范畴。因此,我们可以C(a, a)通过选择一个 1 单元、T和两个 2 单元来定义幺半群:
Now let’s remove all mention of endofunctors from this definition. We start with a bicategory C and pick a 0-cell a in it. As we’ve seen earlier, the hom-category C(a, a) is a monoidal category. We can therefore define a monoid in C(a, a) by picking a 1-cell, T, and two 2-cells:
η :: I -> T
μ :: T ∘ T -> Tη :: I -> T
μ :: T ∘ T -> T
满足幺半群定律。我们称之为单子。
satisfying the monoid laws. We call this a monad.
这是仅使用 0 单元、1 单元和 2 单元的 monad 的更一般定义。当应用于二类Cat时,它会简化为通常的 monad 。但让我们看看其他二类中会发生什么。
That’s a much more general definition of a monad using only 0-cells, 1-cells, and 2-cells. It reduces to the usual monad when applied to the bicategory Cat. But let’s see what happens in other bicategories.
让我们在Span中构造一个 monad 。我们选择一个 0 单元格,这是一个集合,出于很快就会清楚的原因,我将调用Ob。Ob接下来,我们选择一个 end-1-cell:从back 到 的跨度Ob。它在顶点有一个集合,我将其称为Ar,具有两个功能:
Let’s construct a monad in Span. We pick a 0-cell, which is a set that, for reasons that will become clear soon, I will call Ob. Next, we pick an endo-1-cell: a span from Ob back to Ob. It has a set at the apex, which I will call Ar, equipped with two functions:
dom :: Ar -> Ob
cod :: Ar -> Obdom :: Ar -> Ob
cod :: Ar -> Ob
我们将集合的元素称为Ar“箭头”。如果我还告诉你将元素称为Ob“对象”,你可能会得到一个提示,这会导致什么结果。这两个函数dom将cod域和共域分配给“箭头”。
Let’s call the elements of the set Ar “arrows.” If I also tell you to call the elements of Ob “objects,” you might get a hint where this is leading to. The two functions dom and cod assign the domain and the codomain to an “arrow.”
为了使我们的跨度成为一个单子,我们需要两个 2 单元格,η并且μ。在这种情况下,幺半群单位是从Ob到的平凡跨度Ob,其顶点为Ob以及两个恒等函数。2 单元是顶点和η之间的函数。换句话说,为每个“对象”分配一个“箭头”。Span中的 2 节电池必须满足换向条件 — 在本例中:ObArrη
To make our span into a monad, we need two 2-cells, η and μ. The monoidal unit, in this case, is the trivial span from Ob to Ob with the apex at Ob and two identity functions. The 2-cell η is a function between the apices Ob and Arr. In other words, η assigns an “arrow” to every “object.” A 2-cell in Span must satisfy commutation conditions — in this case:
dom ∘ η = id
cod ∘ η = iddom ∘ η = id
cod ∘ η = id
在组件中,这变成:
In components, this becomes:
dom (η ob) = ob = cod (η ob)dom (η ob) = ob = cod (η ob)
其中ob是 中的“对象” Ob。换句话说,η分配给每个“对象”和“箭头”,其域和共域是该“对象”。我们将这种特殊的“箭头”称为“身份箭头”。
where ob is an “object” in Ob. In other words, η assigns to every “object” and “arrow” whose domain and codomain are that “object.” We’ll call this special “arrow” the “identity arrow.”
第二个 2 单元作用于其自身μ跨度的组成。Ar该组合被定义为回调,因此它的元素是来自Ar“箭头”对的元素对(a1, a2)。回调条件为:
The second 2-cell μ acts on the composition of the span Ar with itself. The composition is defined as a pullback, so its elements are pairs of elements from Ar — pairs of “arrows” (a1, a2). The pullback condition is:
cod a1 = dom a2cod a1 = dom a2
我们说 和a1是a1“可组合的”,因为其中一个的域是另一个的共域。
We say that a1 and a1 are “composable,” because the domain of one is the codomain of the other.
2-cellμ是一个将一对可组合箭头映射到的(a1, a2)单个箭头的函数。换句话说,定义了箭头的组合。a3Arμ
The 2-cell μ is a function that maps a pair of composable arrows (a1, a2) to a single arrow a3 from Ar. In other words μ defines composition of arrows.
很容易检查单子定律是否对应于箭头的恒等定律和结合律。我们刚刚定义了一个范畴(请注意,一个小范畴,其中对象和箭头形成集合)。
It’s easy to check that monad laws correspond to identity and associativity laws for arrows. We have just defined a category (a small category, mind you, in which objects and arrows form sets).
因此,总而言之,范畴只是跨度二范畴中的一个单子。
So, all told, a category is just a monad in the bicategory of spans.
这个结果的惊人之处在于,它将范畴与其他代数结构(如单子和幺半群)置于同一基础上。作为一个范畴并没有什么特别的。它只是两组和四个功能。事实上,我们甚至不需要单独的对象集,因为对象可以用恒等箭头来标识(它们是一一对应的)。所以它实际上只是一组和一些函数。考虑到范畴论在所有数学中发挥的关键作用,这是一个非常令人谦卑的认识。
What is amazing about this result is that it puts categories on the same footing as other algebraic structures like monads and monoids. There is nothing special about being a category. It’s just two sets and four functions. In fact we don’t even need a separate set for objects, because objects can be identified with identity arrows (they are in one-to-one correspondence). So it’s really just a set and a few functions. Considering the pivotal role that category theory plays in all of mathematics, this is a very humbling realization.